Skip to content

Commit 30d7cc3

Browse files
Insticator: Add Bidder (#3647)
1 parent d97b5cf commit 30d7cc3

File tree

14 files changed

+1172
-0
lines changed

14 files changed

+1172
-0
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package org.prebid.server.bidder.insticator;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.iab.openrtb.request.App;
5+
import com.iab.openrtb.request.BidRequest;
6+
import com.iab.openrtb.request.Device;
7+
import com.iab.openrtb.request.Imp;
8+
import com.iab.openrtb.request.Publisher;
9+
import com.iab.openrtb.request.Site;
10+
import com.iab.openrtb.request.Video;
11+
import com.iab.openrtb.response.Bid;
12+
import com.iab.openrtb.response.BidResponse;
13+
import com.iab.openrtb.response.SeatBid;
14+
import io.vertx.core.MultiMap;
15+
import org.apache.commons.collections4.CollectionUtils;
16+
import org.prebid.server.bidder.Bidder;
17+
import org.prebid.server.bidder.model.BidderBid;
18+
import org.prebid.server.bidder.model.BidderCall;
19+
import org.prebid.server.bidder.model.BidderError;
20+
import org.prebid.server.bidder.model.HttpRequest;
21+
import org.prebid.server.bidder.model.Price;
22+
import org.prebid.server.bidder.model.Result;
23+
import org.prebid.server.currency.CurrencyConversionService;
24+
import org.prebid.server.exception.PreBidException;
25+
import org.prebid.server.json.DecodeException;
26+
import org.prebid.server.json.JacksonMapper;
27+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
28+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
29+
import org.prebid.server.proto.openrtb.ext.request.insticator.ExtImpInsticator;
30+
import org.prebid.server.proto.openrtb.ext.response.BidType;
31+
import org.prebid.server.util.BidderUtil;
32+
import org.prebid.server.util.HttpUtil;
33+
import org.prebid.server.util.ObjectUtil;
34+
35+
import java.math.BigDecimal;
36+
import java.util.ArrayList;
37+
import java.util.Collection;
38+
import java.util.Collections;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Objects;
43+
import java.util.Optional;
44+
45+
public class InsticatorBidder implements Bidder<BidRequest> {
46+
47+
private static final TypeReference<ExtPrebid<?, ExtImpInsticator>> TYPE_REFERENCE = new TypeReference<>() {
48+
};
49+
50+
private static final String DEFAULT_BIDDER_CURRENCY = "USD";
51+
private static final String INSTICATOR_FIELD = "insticator";
52+
private static final InsticatorExtRequestCaller DEFAULT_INSTICATOR_CALLER =
53+
InsticatorExtRequestCaller.of("Prebid-Server", "n/a");
54+
55+
private final CurrencyConversionService currencyConversionService;
56+
private final String endpointUrl;
57+
private final JacksonMapper mapper;
58+
59+
public InsticatorBidder(CurrencyConversionService currencyConversionService,
60+
String endpointUrl,
61+
JacksonMapper mapper) {
62+
63+
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
64+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
65+
this.mapper = Objects.requireNonNull(mapper);
66+
}
67+
68+
@Override
69+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
70+
final Map<String, List<Imp>> groupedImps = new HashMap<>();
71+
final List<BidderError> errors = new ArrayList<>();
72+
73+
String publisherId = null;
74+
75+
for (Imp imp : request.getImp()) {
76+
try {
77+
validateImp(imp);
78+
final ExtImpInsticator extImp = parseImpExt(imp);
79+
80+
if (publisherId == null) {
81+
publisherId = extImp.getPublisherId();
82+
}
83+
84+
final Imp modifiedImp = modifyImp(request, imp, extImp);
85+
groupedImps.computeIfAbsent(extImp.getAdUnitId(), key -> new ArrayList<>()).add(modifiedImp);
86+
} catch (PreBidException e) {
87+
errors.add(BidderError.badInput(e.getMessage()));
88+
}
89+
}
90+
91+
final BidRequest modifiedRequest = modifyRequest(request, publisherId, errors);
92+
final List<HttpRequest<BidRequest>> requests = groupedImps.values().stream()
93+
.map(imps -> modifiedRequest.toBuilder().imp(imps).build())
94+
.map(finalRequest -> BidderUtil.defaultRequest(
95+
finalRequest,
96+
makeHeaders(finalRequest.getDevice()),
97+
endpointUrl,
98+
mapper))
99+
.toList();
100+
101+
return Result.of(requests, errors);
102+
}
103+
104+
private void validateImp(Imp imp) {
105+
final Video video = imp.getVideo();
106+
if (video == null) {
107+
return;
108+
}
109+
110+
if (isInvalidDimension(video.getH())
111+
|| isInvalidDimension(video.getW())
112+
|| CollectionUtils.isNotEmpty(video.getMimes())) {
113+
114+
throw new PreBidException("One or more invalid or missing video field(s) w, h, mimes");
115+
}
116+
}
117+
118+
private static boolean isInvalidDimension(Integer dimension) {
119+
return dimension == null || dimension == 0;
120+
}
121+
122+
private ExtImpInsticator parseImpExt(Imp imp) {
123+
try {
124+
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
125+
} catch (IllegalArgumentException e) {
126+
throw new PreBidException(e.getMessage());
127+
}
128+
}
129+
130+
private Imp modifyImp(BidRequest request, Imp imp, ExtImpInsticator extImp) {
131+
final Price bidFloorPrice = resolveBidFloor(request, imp);
132+
return imp.toBuilder()
133+
.ext(mapper.mapper().createObjectNode().set(INSTICATOR_FIELD, mapper.mapper().valueToTree(extImp)))
134+
.bidfloorcur(bidFloorPrice.getCurrency())
135+
.bidfloor(bidFloorPrice.getValue())
136+
.build();
137+
}
138+
139+
private Price resolveBidFloor(BidRequest bidRequest, Imp imp) {
140+
final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
141+
return BidderUtil.isValidPrice(initialBidFloorPrice)
142+
? convertBidFloor(initialBidFloorPrice, bidRequest)
143+
: initialBidFloorPrice;
144+
}
145+
146+
private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) {
147+
final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
148+
bidFloorPrice.getValue(),
149+
bidRequest,
150+
bidFloorPrice.getCurrency(),
151+
DEFAULT_BIDDER_CURRENCY);
152+
153+
return Price.of(DEFAULT_BIDDER_CURRENCY, BidderUtil.roundFloor(convertedPrice));
154+
}
155+
156+
private BidRequest modifyRequest(BidRequest request, String publisherId, List<BidderError> errors) {
157+
return request.toBuilder()
158+
.site(modifySite(request.getSite(), publisherId))
159+
.app(modifyApp(request.getApp(), publisherId))
160+
.ext(modifyExtRequest(request.getExt(), errors))
161+
.build();
162+
}
163+
164+
private static Site modifySite(Site site, String id) {
165+
return Optional.ofNullable(site)
166+
.map(Site::toBuilder)
167+
.map(builder -> builder.publisher(modifyPublisher(site.getPublisher(), id)))
168+
.map(Site.SiteBuilder::build)
169+
.orElse(null);
170+
}
171+
172+
private static App modifyApp(App app, String id) {
173+
return Optional.ofNullable(app)
174+
.map(App::toBuilder)
175+
.map(builder -> builder.publisher(modifyPublisher(app.getPublisher(), id)))
176+
.map(App.AppBuilder::build)
177+
.orElse(null);
178+
}
179+
180+
private static Publisher modifyPublisher(Publisher publisher, String id) {
181+
return Optional.ofNullable(publisher)
182+
.map(Publisher::toBuilder)
183+
.orElseGet(Publisher::builder)
184+
.id(id)
185+
.build();
186+
}
187+
188+
private ExtRequest modifyExtRequest(ExtRequest extRequest, List<BidderError> errors) {
189+
final ExtRequest modifiedExtRequest = extRequest == null ? ExtRequest.empty() : extRequest;
190+
final InsticatorExtRequest existingInsticator = getInsticatorExtRequest(modifiedExtRequest, errors);
191+
192+
modifiedExtRequest.addProperty(
193+
INSTICATOR_FIELD,
194+
mapper.mapper().valueToTree(buildInsticator(existingInsticator)));
195+
196+
return modifiedExtRequest;
197+
}
198+
199+
private InsticatorExtRequest getInsticatorExtRequest(ExtRequest modifiedExtRequest, List<BidderError> errors) {
200+
try {
201+
return mapper.mapper().convertValue(
202+
modifiedExtRequest.getProperty(INSTICATOR_FIELD),
203+
InsticatorExtRequest.class);
204+
} catch (IllegalArgumentException e) {
205+
errors.add(BidderError.badInput(e.getMessage()));
206+
return null;
207+
}
208+
}
209+
210+
private static InsticatorExtRequest buildInsticator(InsticatorExtRequest existingInsticator) {
211+
if (existingInsticator == null || CollectionUtils.isEmpty(existingInsticator.getCaller())) {
212+
return InsticatorExtRequest.of(Collections.singletonList(DEFAULT_INSTICATOR_CALLER));
213+
}
214+
215+
final List<InsticatorExtRequestCaller> callers = new ArrayList<>(existingInsticator.getCaller());
216+
callers.add(DEFAULT_INSTICATOR_CALLER);
217+
return InsticatorExtRequest.of(Collections.unmodifiableList(callers));
218+
}
219+
220+
private static MultiMap makeHeaders(Device device) {
221+
final MultiMap headers = HttpUtil.headers();
222+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER,
223+
ObjectUtil.getIfNotNull(device, Device::getUa));
224+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER,
225+
ObjectUtil.getIfNotNull(device, Device::getIp));
226+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, "IP",
227+
ObjectUtil.getIfNotNull(device, Device::getIp));
228+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER,
229+
ObjectUtil.getIfNotNull(device, Device::getIpv6));
230+
231+
return headers;
232+
}
233+
234+
@Override
235+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
236+
try {
237+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
238+
return Result.withValues(extractBids(bidResponse));
239+
} catch (DecodeException | PreBidException e) {
240+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
241+
}
242+
}
243+
244+
private static List<BidderBid> extractBids(BidResponse bidResponse) {
245+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
246+
return Collections.emptyList();
247+
}
248+
249+
return bidResponse.getSeatbid().stream()
250+
.filter(Objects::nonNull)
251+
.map(SeatBid::getBid)
252+
.filter(Objects::nonNull)
253+
.flatMap(Collection::stream)
254+
.filter(Objects::nonNull)
255+
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
256+
.filter(Objects::nonNull)
257+
.toList();
258+
}
259+
260+
private static BidType getBidType(Bid bid) {
261+
return switch (bid.getMtype()) {
262+
case 2 -> BidType.video;
263+
case null, default -> BidType.banner;
264+
};
265+
}
266+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.prebid.server.bidder.insticator;
2+
3+
import lombok.Value;
4+
5+
import java.util.List;
6+
7+
@Value(staticConstructor = "of")
8+
public class InsticatorExtRequest {
9+
10+
List<InsticatorExtRequestCaller> caller;
11+
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.prebid.server.bidder.insticator;
2+
3+
import lombok.Value;
4+
5+
@Value(staticConstructor = "of")
6+
public class InsticatorExtRequestCaller {
7+
8+
String name;
9+
10+
String version;
11+
12+
}
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.insticator;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpInsticator {
8+
9+
@JsonProperty("adUnitId")
10+
String adUnitId;
11+
12+
@JsonProperty("publisherId")
13+
String publisherId;
14+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.insticator.InsticatorBidder;
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.springframework.beans.factory.annotation.Value;
12+
import org.springframework.boot.context.properties.ConfigurationProperties;
13+
import org.springframework.context.annotation.Bean;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.springframework.context.annotation.PropertySource;
16+
17+
import jakarta.validation.constraints.NotBlank;
18+
19+
@Configuration
20+
@PropertySource(value = "classpath:/bidder-config/insticator.yaml", factory = YamlPropertySourceFactory.class)
21+
public class InsticatorConfiguration {
22+
23+
private static final String BIDDER_NAME = "insticator";
24+
25+
@Bean("insticatorConfigurationProperties")
26+
@ConfigurationProperties("adapters.insticator")
27+
BidderConfigurationProperties configurationProperties() {
28+
return new BidderConfigurationProperties();
29+
}
30+
31+
@Bean
32+
BidderDeps insticatorBidderDeps(BidderConfigurationProperties insticatorConfigurationProperties,
33+
@NotBlank @Value("${external-url}") String externalUrl,
34+
CurrencyConversionService currencyConversionService,
35+
JacksonMapper mapper) {
36+
37+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
38+
.withConfig(insticatorConfigurationProperties)
39+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
40+
.bidderCreator(config -> new InsticatorBidder(currencyConversionService, config.getEndpoint(), mapper))
41+
.assemble();
42+
}
43+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
adapters:
2+
insticator:
3+
endpoint: https://ex.ingage.tech/v1/prebidserver
4+
meta-info:
5+
maintainer-email: [email protected]
6+
app-media-types:
7+
- banner
8+
- video
9+
site-media-types:
10+
- banner
11+
- video
12+
supported-vendors:
13+
vendor-id: 910
14+
usersync:
15+
cookie-family-name: insticator
16+
iframe:
17+
url: https://usync.ingage.tech?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}
18+
support-cors: false
19+
uid-macro: '$UID'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Insticator Adapter Params",
4+
"description": "A schema which validates params accepted by Insticator",
5+
"type": "object",
6+
"properties": {
7+
"adUnitId": {
8+
"type": "string",
9+
"description": "Ad Unit Id",
10+
"minLength": 1
11+
},
12+
"publisherId": {
13+
"type": "string",
14+
"description": "Publisher Id",
15+
"minLength": 1
16+
}
17+
},
18+
"required": [
19+
"adUnitId",
20+
"publisherId"
21+
]
22+
}

0 commit comments

Comments
 (0)