Skip to content

Commit a974b78

Browse files
authored
Teal: new adapter (prebid#4350)
1 parent 66d7d8a commit a974b78

12 files changed

Lines changed: 800 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package org.prebid.server.bidder.teal;
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.BidRequest;
8+
import com.iab.openrtb.request.Imp;
9+
import com.iab.openrtb.request.Publisher;
10+
import com.iab.openrtb.request.Site;
11+
import com.iab.openrtb.response.BidResponse;
12+
import com.iab.openrtb.response.SeatBid;
13+
import org.apache.commons.collections4.CollectionUtils;
14+
import org.apache.commons.lang3.ObjectUtils;
15+
import org.apache.commons.lang3.StringUtils;
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.Result;
22+
import org.prebid.server.exception.PreBidException;
23+
import org.prebid.server.json.DecodeException;
24+
import org.prebid.server.json.JacksonMapper;
25+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
26+
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
27+
import org.prebid.server.proto.openrtb.ext.request.teal.ExtImpTeal;
28+
import org.prebid.server.proto.openrtb.ext.response.BidType;
29+
import org.prebid.server.util.BidderUtil;
30+
import org.prebid.server.util.HttpUtil;
31+
32+
import java.util.ArrayList;
33+
import java.util.Collection;
34+
import java.util.Collections;
35+
import java.util.List;
36+
import java.util.Objects;
37+
import java.util.Optional;
38+
39+
public class TealBidder implements Bidder<BidRequest> {
40+
41+
private static final TypeReference<ExtPrebid<?, ExtImpTeal>> TYPE_REFERENCE = new TypeReference<>() {
42+
};
43+
44+
private final String endpointUrl;
45+
private final JacksonMapper mapper;
46+
47+
public TealBidder(String endpointUrl, JacksonMapper mapper) {
48+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
49+
this.mapper = Objects.requireNonNull(mapper);
50+
}
51+
52+
@Override
53+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
54+
final List<Imp> modifiedImps = new ArrayList<>();
55+
final List<BidderError> errors = new ArrayList<>();
56+
String account = null;
57+
58+
for (Imp imp : request.getImp()) {
59+
final ExtImpTeal extImpTeal;
60+
try {
61+
extImpTeal = parseImpExt(imp);
62+
validateImpExt(extImpTeal);
63+
} catch (PreBidException e) {
64+
errors.add(BidderError.badInput(e.getMessage()));
65+
continue;
66+
}
67+
68+
account = account == null ? extImpTeal.getAccount() : account;
69+
modifiedImps.add(modifyImp(imp, extImpTeal.getPlacement()));
70+
}
71+
72+
if (modifiedImps.isEmpty()) {
73+
return Result.withErrors(errors);
74+
}
75+
76+
final BidRequest modifiedRequest = modifyBidRequest(request, account, modifiedImps);
77+
return Result.of(
78+
Collections.singletonList(BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper)),
79+
errors);
80+
}
81+
82+
private ExtImpTeal parseImpExt(Imp imp) {
83+
try {
84+
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
85+
} catch (IllegalArgumentException e) {
86+
throw new PreBidException("Error parsing imp.ext for impression " + imp.getId());
87+
}
88+
}
89+
90+
private static void validateImpExt(ExtImpTeal extImpTeal) {
91+
if (StringUtils.isBlank(extImpTeal.getAccount())) {
92+
throw new PreBidException("account parameter failed validation");
93+
}
94+
95+
final String placement = extImpTeal.getPlacement();
96+
if (placement != null && StringUtils.isBlank(placement)) {
97+
throw new PreBidException("placement parameter failed validation");
98+
}
99+
}
100+
101+
private static Imp modifyImp(Imp imp, String placement) {
102+
if (placement == null) {
103+
return imp;
104+
}
105+
106+
final ObjectNode modifiedExt = imp.getExt().deepCopy();
107+
getOrCreate(getOrCreate(modifiedExt, "prebid"), "storedrequest")
108+
.put("id", placement);
109+
110+
return imp.toBuilder().ext(modifiedExt).build();
111+
}
112+
113+
private static ObjectNode getOrCreate(ObjectNode parent, String field) {
114+
final JsonNode child = parent.get(field);
115+
return child != null && child.isObject()
116+
? (ObjectNode) child
117+
: parent.putObject(field);
118+
}
119+
120+
private BidRequest modifyBidRequest(BidRequest request, String account, List<Imp> modifiedImps) {
121+
final ExtRequest ext = ObjectUtils.defaultIfNull(request.getExt(), ExtRequest.empty());
122+
ext.addProperty("bids", mapper.mapper().createObjectNode().put("pbs", 1));
123+
124+
return request.toBuilder()
125+
.site(modifySite(request.getSite(), account))
126+
.app(modifyApp(request.getApp(), account))
127+
.imp(modifiedImps)
128+
.ext(ext)
129+
.build();
130+
}
131+
132+
private static Site modifySite(Site site, String account) {
133+
return site != null
134+
? site.toBuilder()
135+
.publisher(modifyPublisher(site.getPublisher(), account))
136+
.build()
137+
: null;
138+
}
139+
140+
private static App modifyApp(App app, String account) {
141+
return app != null
142+
? app.toBuilder()
143+
.publisher(modifyPublisher(app.getPublisher(), account))
144+
.build()
145+
: null;
146+
}
147+
148+
private static Publisher modifyPublisher(Publisher publisher, String account) {
149+
return Optional.ofNullable(publisher)
150+
.map(Publisher::toBuilder)
151+
.orElseGet(Publisher::builder)
152+
.id(account)
153+
.build();
154+
}
155+
156+
@Override
157+
public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
158+
try {
159+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
160+
return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse));
161+
} catch (DecodeException e) {
162+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
163+
}
164+
}
165+
166+
private static List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
167+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
168+
return Collections.emptyList();
169+
}
170+
return bidsFromResponse(bidRequest, bidResponse);
171+
}
172+
173+
private static List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
174+
return bidResponse.getSeatbid().stream()
175+
.filter(Objects::nonNull)
176+
.map(SeatBid::getBid)
177+
.filter(Objects::nonNull)
178+
.flatMap(Collection::stream)
179+
.filter(Objects::nonNull)
180+
.map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
181+
.toList();
182+
}
183+
184+
private static BidType getBidType(String impId, List<Imp> imps) {
185+
for (Imp imp : imps) {
186+
if (imp.getId().equals(impId)) {
187+
if (imp.getBanner() != null) {
188+
return BidType.banner;
189+
} else if (imp.getVideo() != null) {
190+
return BidType.video;
191+
} else if (imp.getAudio() != null) {
192+
return BidType.audio;
193+
} else if (imp.getXNative() != null) {
194+
return BidType.xNative;
195+
}
196+
}
197+
}
198+
return BidType.banner;
199+
}
200+
}
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.teal;
2+
3+
import lombok.Value;
4+
5+
@Value(staticConstructor = "of")
6+
public class ExtImpTeal {
7+
8+
String account;
9+
10+
String placement;
11+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.teal.TealBidder;
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/teal.yaml", factory = YamlPropertySourceFactory.class)
20+
public class TealConfiguration {
21+
22+
private static final String BIDDER_NAME = "teal";
23+
24+
@Bean("tealConfigurationProperties")
25+
@ConfigurationProperties("adapters.teal")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps tealBidderDeps(BidderConfigurationProperties tealConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(tealConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new TealBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
adapters:
2+
teal:
3+
ortb-version: "2.6"
4+
endpoint: https://a.bids.ws/openrtb2/auction
5+
modifying-vast-xml-allowed: true
6+
endpoint-compression: gzip
7+
geoscope:
8+
- global
9+
aliases:
10+
tealplus:
11+
enabled: false
12+
ortb:
13+
multiformat-supported: true
14+
meta-info:
15+
maintainer-email: prebid@teal.works
16+
app-media-types:
17+
- banner
18+
- video
19+
- native
20+
site-media-types:
21+
- banner
22+
- video
23+
- native
24+
supported-vendors:
25+
vendor-id: 1378
26+
usersync:
27+
cookie-family-name: teal
28+
iframe:
29+
url: https://bids.ws/load-pbs.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect_url={{redirect_url}}
30+
support-cors: false
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Teal Adapter Params",
4+
"description": "A schema which validates params accepted by the Teal adapter",
5+
"type": "object",
6+
"properties": {
7+
"account": {
8+
"type": "string",
9+
"description": "Account ID"
10+
},
11+
"placement": {
12+
"type": "string",
13+
"description": "Placement ID or name (optional)"
14+
}
15+
},
16+
"required": [
17+
"account"
18+
]
19+
}

0 commit comments

Comments
 (0)