Skip to content

Commit 2e1b6a5

Browse files
add showheroes bid adapter
1 parent 27d6364 commit 2e1b6a5

11 files changed

Lines changed: 867 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package org.prebid.server.bidder.showheroes;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.iab.openrtb.request.Banner;
7+
import com.iab.openrtb.request.BidRequest;
8+
import com.iab.openrtb.request.Imp;
9+
import com.iab.openrtb.request.Video;
10+
import com.iab.openrtb.response.Bid;
11+
import com.iab.openrtb.response.BidResponse;
12+
import com.iab.openrtb.response.SeatBid;
13+
import org.apache.commons.collections4.CollectionUtils;
14+
import org.prebid.server.bidder.Bidder;
15+
import org.prebid.server.bidder.model.BidderBid;
16+
import org.prebid.server.bidder.model.BidderCall;
17+
import org.prebid.server.bidder.model.BidderError;
18+
import org.prebid.server.bidder.model.HttpRequest;
19+
import org.prebid.server.bidder.model.Result;
20+
import org.prebid.server.currency.CurrencyConversionService;
21+
import org.prebid.server.exception.PreBidException;
22+
import org.prebid.server.json.DecodeException;
23+
import org.prebid.server.json.JacksonMapper;
24+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
25+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
26+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
27+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel;
28+
import org.prebid.server.proto.openrtb.ext.request.showheroes.ExtImpShowheroes;
29+
import org.prebid.server.proto.openrtb.ext.response.BidType;
30+
import org.prebid.server.util.BidderUtil;
31+
import org.prebid.server.util.HttpUtil;
32+
import org.prebid.server.version.PrebidVersionProvider;
33+
34+
import java.math.BigDecimal;
35+
import java.util.ArrayList;
36+
import java.util.Collection;
37+
import java.util.Collections;
38+
import java.util.List;
39+
import java.util.Objects;
40+
import java.util.Optional;
41+
42+
public class ShowheroesBidder implements Bidder<BidRequest> {
43+
44+
private static final String BID_CURRENCY = "EUR";
45+
private static final String DEFAULT_ORTB_CURRENCY = "USD";
46+
private static final TypeReference<ExtPrebid<?, ExtImpShowheroes>> SHOWHEROES_EXT_TYPE_REFERENCE =
47+
new TypeReference<>() {
48+
};
49+
50+
private final String endpointUrl;
51+
private final CurrencyConversionService currencyConversionService;
52+
private final JacksonMapper mapper;
53+
54+
public ShowheroesBidder(String endpointUrl,
55+
CurrencyConversionService currencyConversionService,
56+
PrebidVersionProvider prebidVersionProvider,
57+
JacksonMapper mapper) {
58+
59+
this.endpointUrl = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpointUrl));
60+
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
61+
this.mapper = Objects.requireNonNull(mapper);
62+
}
63+
64+
private BidderError validate(BidRequest bidRequest) {
65+
// request must contain site object with page or app object with bundle
66+
if (bidRequest.getSite() == null && bidRequest.getApp() == null) {
67+
return BidderError.badInput("BidRequest must contain one of site or app");
68+
}
69+
if (bidRequest.getSite() != null && bidRequest.getSite().getPage() == null) {
70+
return BidderError.badInput("BidRequest.site.page is required");
71+
}
72+
if (bidRequest.getApp() != null && bidRequest.getApp().getBundle() == null) {
73+
return BidderError.badInput("BidRequest.app.bundle is required");
74+
}
75+
return null;
76+
}
77+
78+
private ExtRequestPrebidChannel getPrebidChannel(BidRequest bidRequest) {
79+
return Optional.ofNullable(bidRequest.getExt())
80+
.map(ExtRequest::getPrebid)
81+
.map(ExtRequestPrebid::getChannel)
82+
.orElse(null);
83+
}
84+
85+
private Imp processImpression(BidRequest bidRequest, Imp imp, ExtRequestPrebidChannel prebidChannel) {
86+
final Banner banner = imp.getBanner();
87+
final Video video = imp.getVideo();
88+
if (banner == null && video == null) {
89+
throw new PreBidException("Impression must contain one of banner, video or native");
90+
}
91+
92+
final ExtImpShowheroes extImpShowheroes = mapper.mapper()
93+
.convertValue(imp.getExt(), SHOWHEROES_EXT_TYPE_REFERENCE).getBidder();
94+
if (extImpShowheroes == null || extImpShowheroes.getUnitId() == null
95+
|| extImpShowheroes.getUnitId().isBlank()) {
96+
throw new PreBidException("Ext.imp.bidder.unitId is required");
97+
}
98+
99+
String channelName = null;
100+
String channelVersion = null;
101+
if (prebidChannel != null) {
102+
channelName = prebidChannel.getName();
103+
channelVersion = prebidChannel.getVersion();
104+
}
105+
106+
final ObjectNode impExt = imp.getExt();
107+
108+
final Imp.ImpBuilder impBuilder = imp.toBuilder();
109+
110+
// copy unitId from ext.bidder to ext.params
111+
impExt.set("params", JsonNodeFactory.instance.objectNode()
112+
.put("unitId", extImpShowheroes.getUnitId()));
113+
114+
impBuilder.ext(impExt);
115+
if (imp.getDisplaymanager() == null && channelName != null) {
116+
impBuilder.displaymanager(channelName);
117+
impBuilder.displaymanagerver(channelVersion);
118+
}
119+
120+
String currency = imp.getBidfloorcur();
121+
// if floor price is 0, or currency is EUR - no need to convert
122+
if (imp.getBidfloor() == null || imp.getBidfloor().compareTo(BigDecimal.ZERO) == 0
123+
|| currency == BID_CURRENCY) {
124+
return impBuilder.build();
125+
}
126+
if (currency != null && !currency.isBlank()) {
127+
// if not provided default currency is USD
128+
currency = DEFAULT_ORTB_CURRENCY;
129+
}
130+
131+
final BigDecimal eurFloor = currencyConversionService.convertCurrency(
132+
imp.getBidfloor(), bidRequest, currency, BID_CURRENCY);
133+
return impBuilder
134+
.bidfloorcur(BID_CURRENCY)
135+
.bidfloor(eurFloor)
136+
.build();
137+
}
138+
139+
@Override
140+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
141+
final BidderError validationError = validate(request);
142+
if (validationError != null) {
143+
return Result.of(Collections.emptyList(), List.of(validationError));
144+
}
145+
146+
final List<BidderError> errors = new ArrayList<>();
147+
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
148+
149+
final ExtRequestPrebidChannel prebidChannel = getPrebidChannel(request);
150+
final List<Imp> modifiedImps = new ArrayList<>(request.getImp().size());
151+
152+
for (Imp impression : request.getImp()) {
153+
try {
154+
modifiedImps.add(processImpression(request, impression, prebidChannel));
155+
} catch (Exception e) {
156+
errors.add(BidderError.badInput(e.getMessage()));
157+
continue;
158+
}
159+
}
160+
161+
httpRequests.add(makeHttpRequest(request.toBuilder().imp(modifiedImps).build()));
162+
return Result.of(httpRequests, errors);
163+
}
164+
165+
private HttpRequest<BidRequest> makeHttpRequest(BidRequest request) {
166+
return BidderUtil.defaultRequest(request, endpointUrl, mapper);
167+
}
168+
169+
@Override
170+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
171+
final BidResponse bidResponse;
172+
final int statusCode = httpCall.getResponse().getStatusCode();
173+
if (statusCode == 204) {
174+
return Result.of(Collections.emptyList(), Collections.emptyList());
175+
}
176+
if (statusCode != 200) {
177+
return Result.withError(BidderError.badServerResponse(
178+
"Unexpected status code: " + statusCode));
179+
}
180+
181+
try {
182+
bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
183+
} catch (DecodeException | PreBidException e) {
184+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
185+
}
186+
187+
return Result.of(extractBids(bidResponse), Collections.emptyList());
188+
}
189+
190+
private List<BidderBid> extractBids(BidResponse bidResponse) {
191+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
192+
return Collections.emptyList();
193+
}
194+
195+
return bidResponse.getSeatbid().stream()
196+
.filter(Objects::nonNull)
197+
.map(SeatBid::getBid)
198+
.filter(Objects::nonNull)
199+
.flatMap(Collection::stream)
200+
.filter(Objects::nonNull)
201+
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
202+
.filter(Objects::nonNull)
203+
.toList();
204+
}
205+
206+
private BidType getBidType(Bid bid) {
207+
return switch (bid.getMtype()) {
208+
case 1 -> BidType.banner;
209+
case 2 -> BidType.video;
210+
default -> BidType.video; // if not provided video is assumed
211+
};
212+
}
213+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.prebid.server.proto.openrtb.ext.request.showheroes;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
/**
7+
* Defines the contract for bidRequest.imp[i].ext.showheroes
8+
*/
9+
@Value(staticConstructor = "of")
10+
public class ExtImpShowheroes {
11+
12+
@JsonProperty("unitId")
13+
String unitId;
14+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.showheroes.ShowheroesBidder;
5+
import org.prebid.server.currency.CurrencyConversionService;
6+
import org.prebid.server.json.JacksonMapper;
7+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
8+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
9+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
10+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
11+
import org.prebid.server.version.PrebidVersionProvider;
12+
import org.springframework.beans.factory.annotation.Value;
13+
import org.springframework.boot.context.properties.ConfigurationProperties;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.context.annotation.PropertySource;
17+
18+
import jakarta.validation.constraints.NotBlank;
19+
20+
@Configuration
21+
@PropertySource(value = "classpath:/bidder-config/showheroes.yaml", factory = YamlPropertySourceFactory.class)
22+
public class ShowheroesConfiguration {
23+
24+
private static final String BIDDER_NAME = "showheroes";
25+
26+
@Bean("showheroesConfigurationProperties")
27+
@ConfigurationProperties("adapters.showheroes")
28+
BidderConfigurationProperties configurationProperties() {
29+
return new BidderConfigurationProperties();
30+
}
31+
32+
@Bean
33+
BidderDeps showheroesBidderDeps(BidderConfigurationProperties showheroesConfigurationProperties,
34+
@NotBlank @Value("${external-url}") String externalUrl,
35+
CurrencyConversionService currencyConversionService,
36+
PrebidVersionProvider prebidVersionProvider,
37+
JacksonMapper mapper) {
38+
39+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
40+
.withConfig(showheroesConfigurationProperties)
41+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
42+
.bidderCreator(config -> new ShowheroesBidder(
43+
config.getEndpoint(),
44+
currencyConversionService,
45+
prebidVersionProvider,
46+
mapper))
47+
.assemble();
48+
}
49+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
adapters:
2+
showheroes:
3+
endpoint: https://ads.viralize.tv/openrtb2/auction/
4+
aliases:
5+
showheroes-bs: ~
6+
showheroesBs: ~
7+
ortb-version: '2.6'
8+
meta-info:
9+
maintainer-email: tech@showheroes.com
10+
app-media-types:
11+
- banner
12+
- video
13+
site-media-types:
14+
- banner
15+
- video
16+
supported-vendors:
17+
vendor-id: 111
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Showheroes Adapter Params",
4+
"description": "A schema which validates params accepted by the Showheroes adapter",
5+
"type": "object",
6+
"properties": {
7+
"unitId": {
8+
"type": "string",
9+
"description": "Unit ID"
10+
}
11+
},
12+
"required": ["unitId"]
13+
}

0 commit comments

Comments
 (0)