Skip to content

Commit 0944054

Browse files
Connatix: Port adapter from PBS-Go (prebid#3781)
1 parent fc6613f commit 0944054

12 files changed

Lines changed: 896 additions & 0 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package org.prebid.server.bidder.connatix;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.iab.openrtb.request.App;
7+
import com.iab.openrtb.request.Banner;
8+
import com.iab.openrtb.request.BidRequest;
9+
import com.iab.openrtb.request.Device;
10+
import com.iab.openrtb.request.Format;
11+
import com.iab.openrtb.request.Imp;
12+
import com.iab.openrtb.response.Bid;
13+
import com.iab.openrtb.response.BidResponse;
14+
import com.iab.openrtb.response.SeatBid;
15+
import io.vertx.core.MultiMap;
16+
import org.apache.commons.collections4.CollectionUtils;
17+
import org.apache.commons.lang3.ObjectUtils;
18+
import org.apache.commons.lang3.StringUtils;
19+
import org.prebid.server.bidder.Bidder;
20+
import org.prebid.server.bidder.model.BidderBid;
21+
import org.prebid.server.bidder.model.BidderCall;
22+
import org.prebid.server.bidder.model.BidderError;
23+
import org.prebid.server.bidder.model.HttpRequest;
24+
import org.prebid.server.bidder.model.Price;
25+
import org.prebid.server.bidder.model.Result;
26+
import org.prebid.server.currency.CurrencyConversionService;
27+
import org.prebid.server.exception.PreBidException;
28+
import org.prebid.server.json.DecodeException;
29+
import org.prebid.server.json.JacksonMapper;
30+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
31+
import org.prebid.server.proto.openrtb.ext.request.ExtApp;
32+
import org.prebid.server.proto.openrtb.ext.request.connatix.ExtImpConnatix;
33+
import org.prebid.server.proto.openrtb.ext.response.BidType;
34+
import org.prebid.server.util.BidderUtil;
35+
import org.prebid.server.util.HttpUtil;
36+
37+
import java.math.BigDecimal;
38+
import java.util.ArrayList;
39+
import java.util.Collection;
40+
import java.util.Collections;
41+
import java.util.List;
42+
import java.util.Objects;
43+
import java.util.Optional;
44+
45+
public class ConnatixBidder implements Bidder<BidRequest> {
46+
47+
private static final TypeReference<ExtPrebid<?, ExtImpConnatix>> CONNATIX_EXT_TYPE_REFERENCE =
48+
new TypeReference<>() {
49+
};
50+
51+
private static final String BIDDER_CURRENCY = "USD";
52+
private static final String FORMATTING = "%s-%s";
53+
54+
private final String endpointUrl;
55+
private final JacksonMapper mapper;
56+
57+
private final CurrencyConversionService currencyConversionService;
58+
59+
public ConnatixBidder(String endpointUrl,
60+
CurrencyConversionService currencyConversionService,
61+
JacksonMapper mapper) {
62+
63+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
64+
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
65+
this.mapper = Objects.requireNonNull(mapper);
66+
}
67+
68+
@Override
69+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
70+
final Device device = request.getDevice();
71+
72+
if (device == null
73+
|| (device.getIp() == null && device.getIpv6() == null)) {
74+
return Result.withError(BidderError.badInput("Device IP is required"));
75+
}
76+
77+
final String displayManagerVer = buildDisplayManagerVersion(request);
78+
final MultiMap headers = resolveHeaders(device);
79+
80+
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
81+
final List<BidderError> errors = new ArrayList<>();
82+
83+
for (Imp imp : request.getImp()) {
84+
try {
85+
final ExtImpConnatix extImpConnatix = parseExtImp(imp);
86+
final Imp modifiedImp = modifyImp(imp, extImpConnatix, displayManagerVer, request);
87+
88+
httpRequests.add(makeHttpRequest(request, modifiedImp, headers));
89+
} catch (PreBidException e) {
90+
errors.add(BidderError.badInput(e.getMessage()));
91+
}
92+
}
93+
94+
return Result.of(httpRequests, errors);
95+
}
96+
97+
private static String buildDisplayManagerVersion(BidRequest request) {
98+
return Optional.ofNullable(request.getApp())
99+
.map(App::getExt)
100+
.map(ExtApp::getPrebid)
101+
.filter(prebid -> ObjectUtils.allNotNull(prebid.getSource(), prebid.getVersion()))
102+
.map(prebid -> FORMATTING.formatted(prebid.getSource(), prebid.getVersion()))
103+
.orElse(StringUtils.EMPTY);
104+
}
105+
106+
private static MultiMap resolveHeaders(Device device) {
107+
final MultiMap headers = HttpUtil.headers();
108+
if (device != null) {
109+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa());
110+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6());
111+
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp());
112+
}
113+
return headers;
114+
}
115+
116+
private ExtImpConnatix parseExtImp(Imp imp) {
117+
try {
118+
return mapper.mapper().convertValue(imp.getExt(), CONNATIX_EXT_TYPE_REFERENCE).getBidder();
119+
} catch (IllegalArgumentException e) {
120+
throw new PreBidException(e.getMessage());
121+
}
122+
}
123+
124+
private Imp modifyImp(Imp imp, ExtImpConnatix extImpConnatix, String displayManagerVer, BidRequest request) {
125+
final Price bidFloorPrice = resolveBidFloor(imp, request);
126+
127+
final ObjectNode impExt = mapper.mapper()
128+
.createObjectNode().set("connatix", mapper.mapper().valueToTree(extImpConnatix));
129+
130+
return imp.toBuilder()
131+
.ext(impExt)
132+
.banner(modifyImpBanner(imp.getBanner()))
133+
.displaymanagerver(StringUtils.isBlank(imp.getDisplaymanagerver())
134+
&& StringUtils.isNotBlank(displayManagerVer)
135+
? displayManagerVer
136+
: imp.getDisplaymanagerver())
137+
.bidfloor(bidFloorPrice.getValue())
138+
.bidfloorcur(bidFloorPrice.getCurrency())
139+
.build();
140+
}
141+
142+
private Price resolveBidFloor(Imp imp, BidRequest bidRequest) {
143+
final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
144+
return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY)
145+
? convertBidFloor(initialBidFloorPrice, bidRequest)
146+
: initialBidFloorPrice;
147+
}
148+
149+
private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) {
150+
final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
151+
bidFloorPrice.getValue(),
152+
bidRequest,
153+
bidFloorPrice.getCurrency(),
154+
BIDDER_CURRENCY);
155+
156+
return Price.of(BIDDER_CURRENCY, convertedPrice);
157+
}
158+
159+
private Banner modifyImpBanner(Banner banner) {
160+
if (banner == null) {
161+
return null;
162+
}
163+
164+
if (banner.getW() == null && banner.getH() == null && CollectionUtils.isNotEmpty(banner.getFormat())) {
165+
final Format firstFormat = banner.getFormat().getFirst();
166+
return banner.toBuilder()
167+
.w(firstFormat.getW())
168+
.h(firstFormat.getH())
169+
.build();
170+
}
171+
return banner;
172+
}
173+
174+
private HttpRequest<BidRequest> makeHttpRequest(BidRequest request, Imp imp, MultiMap headers) {
175+
final BidRequest outgoingRequest = request.toBuilder()
176+
.imp(List.of(imp))
177+
.build();
178+
179+
return BidderUtil.defaultRequest(outgoingRequest, headers, endpointUrl, mapper);
180+
}
181+
182+
@Override
183+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
184+
try {
185+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
186+
final List<BidderBid> bids = extractBids(bidResponse);
187+
188+
return Result.withValues(bids);
189+
} catch (DecodeException | PreBidException e) {
190+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
191+
}
192+
}
193+
194+
private static List<BidderBid> extractBids(BidResponse bidResponse) {
195+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
196+
return Collections.emptyList();
197+
}
198+
199+
return bidResponse.getSeatbid().stream()
200+
.filter(Objects::nonNull)
201+
.map(SeatBid::getBid)
202+
.filter(Objects::nonNull)
203+
.flatMap(Collection::stream)
204+
.filter(Objects::nonNull)
205+
.map(bid -> BidderBid.of(bid, getBidType(bid), BIDDER_CURRENCY))
206+
.toList();
207+
}
208+
209+
private static BidType getBidType(Bid bid) {
210+
return Optional.ofNullable(bid.getExt())
211+
.map(ext -> ext.get("connatix"))
212+
.map(cnx -> cnx.get("mediaType"))
213+
.map(JsonNode::asText)
214+
.filter(type -> Objects.equals(type, "video"))
215+
.map(ignored -> BidType.video)
216+
.orElse(BidType.banner);
217+
}
218+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.prebid.server.proto.openrtb.ext.request.connatix;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
import java.math.BigDecimal;
7+
8+
@Value(staticConstructor = "of")
9+
public class ExtImpConnatix {
10+
11+
@JsonProperty("placementId")
12+
String placementId;
13+
14+
@JsonProperty("viewabilityPercentage")
15+
BigDecimal viewabilityPercentage;
16+
17+
}
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.connatix.ConnatixBidder;
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/connatix.yaml", factory = YamlPropertySourceFactory.class)
21+
public class ConnatixConfiguration {
22+
23+
private static final String BIDDER_NAME = "connatix";
24+
25+
@Bean("connatixConfigurationProperties")
26+
@ConfigurationProperties("adapters.connatix")
27+
BidderConfigurationProperties configurationProperties() {
28+
return new BidderConfigurationProperties();
29+
}
30+
31+
@Bean
32+
BidderDeps connatixBidderDeps(BidderConfigurationProperties connatixConfigurationProperties,
33+
@NotBlank @Value("${external-url}") String externalUrl,
34+
JacksonMapper mapper,
35+
CurrencyConversionService currencyConversionService) {
36+
37+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
38+
.withConfig(connatixConfigurationProperties)
39+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
40+
.bidderCreator(config -> new ConnatixBidder(config.getEndpoint(), currencyConversionService, mapper))
41+
.assemble();
42+
}
43+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
adapters:
2+
connatix:
3+
endpoint: "https://capi.connatix.com/rtb/ortb"
4+
meta-info:
5+
maintainer-email: "pubsolutions@connatix.com"
6+
vendor-id: 143
7+
app-media-types:
8+
- banner
9+
- video
10+
site-media-types:
11+
- banner
12+
- video
13+
usersync:
14+
cookie-family-name: connatix
15+
iframe:
16+
url: "https://capi.connatix.com/us/pixel?pId=53&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}"
17+
uid-macro: '[UID]'
18+
support-cors: false
19+
redirect:
20+
url: "https://capi.connatix.com/us/pixel?pId=52&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}"
21+
uid-macro: '[UID]'
22+
support-cors: false
23+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Connatix Adapter Params",
4+
"description": "A schema which validates params accepted by the Connatix adapter",
5+
"type": "object",
6+
"properties": {
7+
"placementId": {
8+
"type": "string",
9+
"minLength": 1,
10+
"description": "Placement ID"
11+
},
12+
"viewabilityPercentage": {
13+
"type": "number",
14+
"description": "Declared viewability percentage (values from 0 to 1, where 1 = 100%)",
15+
"minimum": 0,
16+
"maximum": 1
17+
}
18+
},
19+
"required": ["placementId"]
20+
}
21+

0 commit comments

Comments
 (0)