Skip to content

Commit 3b1abb9

Browse files
New Adapter: Nativery
1 parent 19e3285 commit 3b1abb9

12 files changed

Lines changed: 1049 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package org.prebid.server.bidder.nativery;
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.BidRequest;
7+
import com.iab.openrtb.request.Imp;
8+
import com.iab.openrtb.response.Bid;
9+
import com.iab.openrtb.response.BidResponse;
10+
import com.iab.openrtb.response.SeatBid;
11+
import io.vertx.core.MultiMap;
12+
import org.apache.commons.collections4.CollectionUtils;
13+
import org.apache.commons.lang3.StringUtils;
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.exception.PreBidException;
21+
import org.prebid.server.json.DecodeException;
22+
import org.prebid.server.json.JacksonMapper;
23+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
24+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
25+
import org.prebid.server.proto.openrtb.ext.request.ExtSite;
26+
import org.prebid.server.proto.openrtb.ext.request.nativery.ExtImpNativery;
27+
import org.prebid.server.proto.openrtb.ext.response.BidType;
28+
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
29+
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta;
30+
import org.prebid.server.util.BidderUtil;
31+
import org.prebid.server.util.HttpUtil;
32+
33+
import java.util.ArrayList;
34+
import java.util.Collection;
35+
import java.util.Collections;
36+
import java.util.List;
37+
import java.util.Objects;
38+
39+
public class NativeryBidder implements Bidder<BidRequest> {
40+
41+
private static final TypeReference<ExtPrebid<?, ExtImpNativery>> NATIVERY_EXT_TYPE_REFERENCE =
42+
new TypeReference<>() {
43+
};
44+
45+
private static final TypeReference<ExtPrebid<ExtBidPrebid, ?>> EXT_PREBID_TYPE_REFERENCE =
46+
new TypeReference<>() {
47+
};
48+
49+
private static final String DEFAULT_CURRENCY = "EUR";
50+
private static final String NATIVERY_ERROR_HEADER = "X-Nativery-Error";
51+
52+
private final String endpointUrl;
53+
private final JacksonMapper mapper;
54+
55+
public NativeryBidder(String endpointUrl, JacksonMapper mapper) {
56+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
57+
this.mapper = Objects.requireNonNull(mapper);
58+
}
59+
60+
@Override
61+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
62+
final List<HttpRequest<BidRequest>> httpRequests = new ArrayList<>();
63+
final List<BidderError> errors = new ArrayList<>();
64+
65+
final boolean isAmp = isAmpRequest(request);
66+
67+
final List<Imp> validImps = new ArrayList<>();
68+
String widgetId = null;
69+
70+
for (Imp imp : request.getImp()) {
71+
try {
72+
final ExtImpNativery extImp = parseImpExt(imp);
73+
if (widgetId == null && extImp != null && StringUtils.isNotBlank(extImp.getWidgetId())) {
74+
widgetId = extImp.getWidgetId();
75+
}
76+
validImps.add(imp);
77+
} catch (PreBidException e) {
78+
errors.add(BidderError.badInput(e.getMessage()));
79+
}
80+
}
81+
82+
if (validImps.isEmpty()) {
83+
return Result.of(Collections.emptyList(), errors);
84+
}
85+
86+
final ExtRequest updatedExt = buildRequestExtWithNativery(request.getExt(), isAmp, widgetId);
87+
88+
for (Imp imp : validImps) {
89+
final BidRequest singleImpRequest = request.toBuilder()
90+
.imp(Collections.singletonList(imp))
91+
.ext(updatedExt)
92+
.cur(Collections.singletonList(DEFAULT_CURRENCY))
93+
.build();
94+
95+
httpRequests.add(BidderUtil.defaultRequest(singleImpRequest, endpointUrl, mapper));
96+
}
97+
98+
return Result.of(httpRequests, errors);
99+
}
100+
101+
private ExtImpNativery parseImpExt(Imp imp) {
102+
try {
103+
final ExtPrebid<?, ExtImpNativery> ext =
104+
mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE);
105+
return ext != null ? ext.getBidder() : null;
106+
} catch (IllegalArgumentException e) {
107+
throw new PreBidException(e.getMessage());
108+
}
109+
}
110+
111+
private ExtRequest buildRequestExtWithNativery(ExtRequest originalExt, boolean isAmp, String widgetId) {
112+
final ObjectNode extNode = mapper.mapper().convertValue(originalExt, ObjectNode.class);
113+
final ObjectNode root = extNode != null ? extNode : mapper.mapper().createObjectNode();
114+
115+
final ObjectNode nativeryNode = root.with("nativery");
116+
nativeryNode.put("isAmp", isAmp);
117+
if (widgetId != null) {
118+
nativeryNode.put("widgetId", widgetId);
119+
}
120+
121+
return mapper.mapper().convertValue(root, ExtRequest.class);
122+
}
123+
124+
private boolean isAmpRequest(BidRequest request) {
125+
if (request.getSite() == null || request.getSite().getExt() == null) {
126+
return false;
127+
}
128+
try {
129+
final ExtSite extSite = mapper.mapper().convertValue(request.getSite().getExt(), ExtSite.class);
130+
final Integer amp = extSite != null ? extSite.getAmp() : null;
131+
return amp != null && amp == 1;
132+
} catch (Exception e) {
133+
return false;
134+
}
135+
}
136+
137+
@Override
138+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
139+
final List<BidderError> errors = new ArrayList<>();
140+
141+
if (httpCall.getResponse() != null && httpCall.getResponse().getStatusCode() == 204) {
142+
final MultiMap headers = httpCall.getResponse().getHeaders();
143+
final String nativeryErr = headers != null ? headers.get(NATIVERY_ERROR_HEADER) : null;
144+
if (StringUtils.isNotBlank(nativeryErr)) {
145+
return Result.withError(BidderError.badInput("Nativery Error: " + nativeryErr + "."));
146+
}
147+
return Result.withError(BidderError.badServerResponse("No Content"));
148+
}
149+
150+
try {
151+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
152+
final List<BidderBid> bidderBids = extractBids(bidResponse, errors);
153+
return Result.of(bidderBids, errors);
154+
} catch (DecodeException e) {
155+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
156+
}
157+
}
158+
159+
private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
160+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
161+
return Collections.emptyList();
162+
}
163+
164+
final String currency = StringUtils.defaultIfBlank(bidResponse.getCur(), DEFAULT_CURRENCY);
165+
166+
return bidResponse.getSeatbid().stream()
167+
.filter(Objects::nonNull)
168+
.map(SeatBid::getBid)
169+
.filter(Objects::nonNull)
170+
.flatMap(Collection::stream)
171+
.filter(Objects::nonNull)
172+
.map(bid -> resolveBidderBid(bid, currency, errors))
173+
.filter(Objects::nonNull)
174+
.toList();
175+
}
176+
177+
private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
178+
try {
179+
final ObjectNode nativeryExt = extractNativeryExt(bid.getExt());
180+
final String mediaTypeRaw = getText(nativeryExt, "bid_ad_media_type");
181+
final BidType bidType = mapMediaType(mediaTypeRaw);
182+
183+
final List<String> advDomains = readStringArray(nativeryExt, "bid_adv_domains");
184+
final Bid updatedBid = addBidMeta(bid, mediaTypeString(bidType), advDomains);
185+
186+
return BidderBid.of(updatedBid, bidType, currency);
187+
} catch (PreBidException e) {
188+
errors.add(BidderError.badInput(e.getMessage()));
189+
return null;
190+
}
191+
}
192+
193+
private ObjectNode extractNativeryExt(ObjectNode bidExt) {
194+
if (bidExt == null) {
195+
throw new PreBidException("missing bid.ext");
196+
}
197+
final JsonNode node = bidExt.get("nativery");
198+
if (!(node instanceof ObjectNode nativeryNode)) {
199+
throw new PreBidException("missing bid.ext.nativery");
200+
}
201+
return nativeryNode;
202+
}
203+
204+
private static String getText(ObjectNode node, String field) {
205+
final JsonNode v = node.get(field);
206+
return v != null && v.isTextual() ? v.asText() : null;
207+
}
208+
209+
private static List<String> readStringArray(ObjectNode node, String field) {
210+
final JsonNode arr = node.get(field);
211+
if (arr == null || !arr.isArray()) {
212+
return Collections.emptyList();
213+
}
214+
final List<String> out = new ArrayList<>();
215+
arr.forEach(e -> {
216+
if (e != null && e.isTextual()) {
217+
out.add(e.asText());
218+
}
219+
});
220+
return out;
221+
}
222+
223+
private static BidType mapMediaType(String mediaType) {
224+
final String mt = StringUtils.defaultString(mediaType).toLowerCase();
225+
return switch (mt) {
226+
case "native" -> BidType.xNative;
227+
case "display", "banner", "rich_media" -> BidType.banner;
228+
case "video" -> BidType.video;
229+
default -> throw new PreBidException(
230+
"unrecognized bid_ad_media_type in response from nativery: " + mediaType);
231+
};
232+
}
233+
234+
private static String mediaTypeString(BidType type) {
235+
return switch (type) {
236+
case banner -> "banner";
237+
case video -> "video";
238+
case xNative -> "native";
239+
default -> throw new IllegalStateException("Unexpected value: " + type);
240+
};
241+
}
242+
243+
private Bid addBidMeta(Bid bid, String mediaType, List<String> advDomains) {
244+
final ExtBidPrebid prebid = parseExtBidPrebid(bid);
245+
246+
final ExtBidPrebidMeta modifiedMeta = (prebid != null && prebid.getMeta() != null
247+
? prebid.getMeta().toBuilder()
248+
: ExtBidPrebidMeta.builder())
249+
.mediaType(mediaType)
250+
.advertiserDomains(advDomains)
251+
.build();
252+
253+
final ExtBidPrebid modifiedPrebid = (prebid != null ? prebid.toBuilder() : ExtBidPrebid.builder())
254+
.meta(modifiedMeta)
255+
.build();
256+
257+
return bid.toBuilder()
258+
.ext(mapper.mapper().valueToTree(ExtPrebid.of(modifiedPrebid, null)))
259+
.build();
260+
}
261+
262+
private ExtBidPrebid parseExtBidPrebid(Bid bid) {
263+
try {
264+
return mapper.mapper()
265+
.convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE)
266+
.getPrebid();
267+
} catch (IllegalArgumentException e) {
268+
return null;
269+
}
270+
}
271+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.prebid.server.proto.openrtb.ext.request.nativery;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpNativery {
8+
9+
@JsonProperty("widgetId")
10+
String widgetId;
11+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.nativery.NativeryBidder;
5+
import org.prebid.server.json.JacksonMapper;
6+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
7+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
8+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
9+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.boot.context.properties.ConfigurationProperties;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.context.annotation.PropertySource;
15+
16+
import jakarta.validation.constraints.NotBlank;
17+
18+
@Configuration
19+
@PropertySource(value = "classpath:/bidder-config/nativery.yaml", factory = YamlPropertySourceFactory.class)
20+
public class NativeryBidderConfiguration {
21+
22+
private static final String BIDDER_NAME = "nativery";
23+
24+
@Bean("nativeryConfigurationProperties")
25+
@ConfigurationProperties("adapters.nativery")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(nativeryConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new NativeryBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
adapters:
2+
nativery:
3+
endpoint: "https://hb.nativery.com/openrtb2/auction"
4+
ortb-version: "2.6"
5+
meta-info:
6+
maintainer-email: "developer@nativery.com"
7+
site-media-types:
8+
- banner
9+
- video
10+
- native
11+
supported-vendors: []
12+
vendor-id: 0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Nativery Adapter Params",
4+
"description": "A schema which validates params accepted by the Nativery adapter",
5+
6+
"type": "object",
7+
"properties": {
8+
"widgetId": {
9+
"type": "string",
10+
"description": "An ID which identifies this Nativery widget"
11+
}
12+
},
13+
14+
"required": ["widgetId"]
15+
}

0 commit comments

Comments
 (0)