Skip to content

Commit ecba19c

Browse files
committed
teal bidder - initial commit
1 parent 18899b8 commit ecba19c

12 files changed

Lines changed: 841 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package org.prebid.server.bidder.teal;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.node.ObjectNode;
5+
import com.iab.openrtb.request.App;
6+
import com.iab.openrtb.request.BidRequest;
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.response.BidResponse;
11+
import com.iab.openrtb.response.SeatBid;
12+
import io.vertx.core.http.HttpMethod;
13+
import org.apache.commons.collections4.CollectionUtils;
14+
import org.apache.commons.lang3.StringUtils;
15+
import org.prebid.server.bidder.Bidder;
16+
import org.prebid.server.bidder.model.BidderBid;
17+
import org.prebid.server.bidder.model.BidderCall;
18+
import org.prebid.server.bidder.model.BidderError;
19+
import org.prebid.server.bidder.model.HttpRequest;
20+
import org.prebid.server.bidder.model.Result;
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.teal.ExtImpTeal;
27+
import org.prebid.server.proto.openrtb.ext.response.BidType;
28+
import org.prebid.server.util.BidderUtil;
29+
import org.prebid.server.util.HttpUtil;
30+
31+
import java.util.ArrayList;
32+
import java.util.Collection;
33+
import java.util.Collections;
34+
import java.util.List;
35+
import java.util.Objects;
36+
import java.util.Optional;
37+
import java.util.stream.Collectors;
38+
39+
public class TealBidder implements Bidder<BidRequest> {
40+
41+
private static final TypeReference<ExtPrebid<?, ExtImpTeal>> TYPE_REFERENCE = new TypeReference<>() {
42+
};
43+
private final String endpointUrl;
44+
private final JacksonMapper mapper;
45+
46+
public TealBidder(String endpointUrl,
47+
JacksonMapper mapper) {
48+
49+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
50+
this.mapper = Objects.requireNonNull(mapper);
51+
}
52+
53+
@Override
54+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
55+
final ExtImpTeal params;
56+
try {
57+
params = parseImpExt(request.getImp().getFirst());
58+
} catch (PreBidException e) {
59+
return Result.withError(BidderError.badInput(e.getMessage()));
60+
}
61+
final String account = params.getAccount();
62+
if (!isParameterValid(account)) {
63+
return Result.withError(BidderError.badInput("account parameter failed validation"));
64+
}
65+
66+
final List<Imp> modifiedImps = new ArrayList<>();
67+
final List<BidderError> errors = new ArrayList<>();
68+
69+
for (Imp imp : request.getImp()) {
70+
final ExtImpTeal impParams;
71+
try {
72+
impParams = parseImpExt(imp);
73+
} catch (PreBidException e) {
74+
errors.add(BidderError.badInput(e.getMessage()));
75+
continue;
76+
}
77+
78+
final String placement = impParams.getPlacement();
79+
final boolean passedValidation = placement == null || isParameterValid(placement);
80+
81+
if (placement != null && !passedValidation) {
82+
errors.add(BidderError.badInput("placement parameter failed validation"));
83+
continue;
84+
}
85+
86+
if (placement != null) {
87+
final ObjectNode ext = Optional.ofNullable(imp.getExt()).orElse(mapper.mapper().createObjectNode());
88+
final ObjectNode prebid = ext.has("prebid") && ext.get("prebid").isObject()
89+
? (ObjectNode) ext.get("prebid")
90+
: ext.putObject("prebid");
91+
prebid.putObject("storedrequest").put("id", placement);
92+
modifiedImps.add(imp.toBuilder().ext(ext).build());
93+
} else {
94+
modifiedImps.add(imp);
95+
}
96+
}
97+
98+
if (CollectionUtils.isEmpty(modifiedImps)) {
99+
return Result.withErrors(errors);
100+
}
101+
102+
final BidRequest modifiedRequest = enrichRequest(request, account, modifiedImps);
103+
return Result.of(List.of(HttpRequest.<BidRequest>builder()
104+
.method(HttpMethod.POST)
105+
.uri(endpointUrl)
106+
.impIds(BidderUtil.impIds(modifiedRequest))
107+
.body(mapper.encodeToBytes(modifiedRequest))
108+
.payload(modifiedRequest)
109+
.build()), errors);
110+
}
111+
112+
private ExtImpTeal parseImpExt(Imp imp) {
113+
try {
114+
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
115+
} catch (IllegalArgumentException e) {
116+
throw new PreBidException("Error parsing imp.ext for impression " + imp.getId());
117+
}
118+
}
119+
120+
private boolean isParameterValid(String parameter) {
121+
return !StringUtils.isBlank(parameter);
122+
}
123+
124+
private BidRequest enrichRequest(BidRequest request, String account, List<Imp> modifiedImps) {
125+
final ExtRequest ext = Optional.ofNullable(request.getExt()).orElse(ExtRequest.empty());
126+
final ObjectNode bids = mapper.mapper().createObjectNode();
127+
bids.put("pbs", 1);
128+
ext.addProperty("bids", bids);
129+
return request.toBuilder()
130+
.site(modifySite(request.getSite(), account))
131+
.app(modifyApp(request.getApp(), account))
132+
.imp(modifiedImps)
133+
.ext(ext)
134+
.build();
135+
}
136+
137+
private static Site modifySite(Site site, String account) {
138+
return account != null && site != null
139+
? site.toBuilder()
140+
.publisher(modifyPublisher(site.getPublisher(), account))
141+
.build()
142+
: site;
143+
}
144+
145+
private static App modifyApp(App app, String account) {
146+
return account != null && app != null
147+
? app.toBuilder()
148+
.publisher(modifyPublisher(app.getPublisher(), account))
149+
.build()
150+
: app;
151+
}
152+
153+
private static Publisher modifyPublisher(Publisher publisher, String account) {
154+
return publisher != null
155+
? publisher.toBuilder().id(account).build()
156+
: Publisher.builder().id(account).build();
157+
}
158+
159+
@Override
160+
public final Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
161+
try {
162+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
163+
return Result.of(extractBids(httpCall.getRequest().getPayload(), bidResponse), Collections.emptyList());
164+
} catch (DecodeException | PreBidException e) {
165+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
166+
}
167+
}
168+
169+
private static List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) {
170+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
171+
return Collections.emptyList();
172+
}
173+
return bidsFromResponse(bidRequest, bidResponse);
174+
}
175+
176+
private static List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse) {
177+
return bidResponse.getSeatbid().stream()
178+
.filter(Objects::nonNull)
179+
.map(SeatBid::getBid)
180+
.filter(Objects::nonNull)
181+
.flatMap(Collection::stream)
182+
.map(bid -> BidderBid.of(bid, getBidType(bid.getImpid(), bidRequest.getImp()), bidResponse.getCur()))
183+
.collect(Collectors.toList());
184+
}
185+
186+
private static BidType getBidType(String impId, List<Imp> imps) {
187+
for (Imp imp : imps) {
188+
if (imp.getId().equals(impId)) {
189+
if (imp.getBanner() != null) {
190+
return BidType.banner;
191+
} else if (imp.getVideo() != null) {
192+
return BidType.video;
193+
} else if (imp.getAudio() != null) {
194+
return BidType.audio;
195+
} else if (imp.getXNative() != null) {
196+
return BidType.xNative;
197+
}
198+
}
199+
}
200+
return BidType.banner;
201+
}
202+
}
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.teal;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpTeal {
8+
9+
@JsonProperty("account")
10+
String account;
11+
12+
@JsonProperty("placement")
13+
String placement;
14+
}
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)