Skip to content

Commit c2f1822

Browse files
committed
New Adapter: Sparteo
1 parent 1e66556 commit c2f1822

File tree

12 files changed

+1511
-0
lines changed

12 files changed

+1511
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package org.prebid.server.bidder.sparteo;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
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.Bid;
12+
import com.iab.openrtb.response.BidResponse;
13+
import com.iab.openrtb.response.SeatBid;
14+
import org.apache.commons.collections4.CollectionUtils;
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.ExtPublisher;
26+
import org.prebid.server.proto.openrtb.ext.request.sparteo.ExtImpSparteo;
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.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.Iterator;
36+
import java.util.List;
37+
import java.util.Map;
38+
import java.util.Objects;
39+
import java.util.Optional;
40+
41+
public class SparteoBidder implements Bidder<BidRequest> {
42+
43+
private static final TypeReference<ExtPrebid<?, ExtImpSparteo>> TYPE_REFERENCE =
44+
new TypeReference<>() { };
45+
46+
private final String endpointUrl;
47+
private final JacksonMapper mapper;
48+
49+
public SparteoBidder(String endpointUrl, JacksonMapper mapper) {
50+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
51+
this.mapper = Objects.requireNonNull(mapper);
52+
}
53+
54+
@Override
55+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
56+
final List<BidderError> errors = new ArrayList<>();
57+
final List<Imp> modifiedImps = new ArrayList<>();
58+
String siteNetworkId = null;
59+
60+
for (Imp imp : request.getImp()) {
61+
try {
62+
final ExtImpSparteo bidderParams = parseExtImp(imp);
63+
64+
if (siteNetworkId == null && bidderParams.getNetworkId() != null) {
65+
siteNetworkId = bidderParams.getNetworkId();
66+
}
67+
68+
final ObjectNode modifiedExt = modifyImpExt(imp);
69+
70+
modifiedImps.add(imp.toBuilder().ext(modifiedExt).build());
71+
} catch (PreBidException e) {
72+
errors.add(BidderError.badInput(
73+
"ignoring imp id=%s, error processing ext: %s".formatted(
74+
imp.getId(), e.getMessage())));
75+
}
76+
}
77+
78+
if (modifiedImps.isEmpty()) {
79+
return Result.withErrors(errors);
80+
}
81+
82+
final BidRequest outgoingRequest = request.toBuilder()
83+
.imp(modifiedImps)
84+
.site(modifySite(request.getSite(), siteNetworkId, mapper))
85+
.build();
86+
87+
final HttpRequest<BidRequest> call = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper);
88+
89+
return Result.of(Collections.singletonList(call), errors);
90+
}
91+
92+
private ExtImpSparteo parseExtImp(Imp imp) {
93+
try {
94+
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
95+
} catch (IllegalArgumentException e) {
96+
throw new PreBidException("invalid imp.ext");
97+
}
98+
}
99+
100+
private static ObjectNode modifyImpExt(Imp imp) {
101+
final ObjectNode modifiedImpExt = imp.getExt().deepCopy();
102+
final JsonNode sparteoJsonNode = modifiedImpExt.get("sparteo");
103+
final ObjectNode sparteoNode = sparteoJsonNode == null || !sparteoJsonNode.isObject()
104+
? modifiedImpExt.putObject("sparteo")
105+
: (ObjectNode) sparteoJsonNode;
106+
107+
final JsonNode paramsJsonNode = sparteoNode.get("params");
108+
final ObjectNode paramsNode = paramsJsonNode == null || !paramsJsonNode.isObject()
109+
? sparteoNode.putObject("params")
110+
: (ObjectNode) paramsJsonNode;
111+
112+
final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder");
113+
if (bidderJsonNode != null && bidderJsonNode.isObject()) {
114+
final Iterator<Map.Entry<String, JsonNode>> fields = bidderJsonNode.fields();
115+
while (fields.hasNext()) {
116+
final Map.Entry<String, JsonNode> field = fields.next();
117+
paramsNode.set(field.getKey(), field.getValue());
118+
}
119+
}
120+
return modifiedImpExt;
121+
}
122+
123+
private Site modifySite(Site site, String siteNetworkId, JacksonMapper mapper) {
124+
if (site == null || site.getPublisher() == null || siteNetworkId == null) {
125+
return site;
126+
}
127+
128+
final Publisher publisher = site.getPublisher();
129+
130+
final ExtPublisher extPublisher = publisher.getExt() != null
131+
? publisher.getExt()
132+
: ExtPublisher.empty();
133+
134+
final JsonNode paramsProperty = extPublisher.getProperty("params");
135+
final ObjectNode paramsNode;
136+
137+
if (paramsProperty != null && paramsProperty.isObject()) {
138+
paramsNode = (ObjectNode) paramsProperty;
139+
} else {
140+
paramsNode = mapper.mapper().createObjectNode();
141+
extPublisher.addProperty("params", paramsNode);
142+
}
143+
144+
paramsNode.put("networkId", siteNetworkId);
145+
146+
final Publisher modifiedPublisher = publisher.toBuilder()
147+
.ext(extPublisher)
148+
.build();
149+
150+
return site.toBuilder()
151+
.publisher(modifiedPublisher)
152+
.build();
153+
}
154+
155+
@Override
156+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
157+
try {
158+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
159+
final List<BidderError> errors = new ArrayList<>();
160+
return Result.of(extractBids(bidResponse, errors), errors);
161+
} catch (DecodeException e) {
162+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
163+
}
164+
}
165+
166+
private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
167+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
168+
return Collections.emptyList();
169+
}
170+
171+
return bidResponse.getSeatbid().stream()
172+
.filter(Objects::nonNull)
173+
.map(SeatBid::getBid)
174+
.filter(Objects::nonNull)
175+
.flatMap(Collection::stream)
176+
.filter(Objects::nonNull)
177+
.map(bid -> toBidderBid(bid, bidResponse.getCur(), errors))
178+
.filter(Objects::nonNull)
179+
.toList();
180+
}
181+
182+
private BidType getBidType(Bid bid) throws PreBidException {
183+
final BidType bidType = Optional.ofNullable(bid.getExt())
184+
.map(ext -> ext.get("prebid"))
185+
.filter(JsonNode::isObject)
186+
.map(this::parseExtBidPrebid)
187+
.map(ExtBidPrebid::getType)
188+
.orElseThrow(() -> new PreBidException(
189+
"Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid())));
190+
191+
if (bidType == BidType.audio) {
192+
throw new PreBidException(
193+
"Audio bid type not supported by this adapter for impression id: %s".formatted(bid.getImpid()));
194+
}
195+
196+
return bidType;
197+
}
198+
199+
private ExtBidPrebid parseExtBidPrebid(JsonNode prebidNode) {
200+
try {
201+
return mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class);
202+
} catch (JsonProcessingException e) {
203+
return null;
204+
}
205+
}
206+
207+
private BidderBid toBidderBid(Bid bid, String currency, List<BidderError> errors) {
208+
try {
209+
final BidType bidType = getBidType(bid);
210+
211+
final Integer mtype = switch (bidType) {
212+
case banner -> 1;
213+
case video -> 2;
214+
case xNative -> 4;
215+
default -> null;
216+
};
217+
218+
final Bid bidWithMtype = mtype != null ? bid.toBuilder().mtype(mtype).build() : bid;
219+
220+
return BidderBid.of(bidWithMtype, bidType, currency);
221+
} catch (PreBidException e) {
222+
errors.add(BidderError.badServerResponse(e.getMessage()));
223+
return null;
224+
}
225+
}
226+
}
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.sparteo;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpSparteo {
8+
9+
@JsonProperty("networkId")
10+
String networkId;
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.sparteo.SparteoBidder;
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/sparteo.yaml",
20+
factory = YamlPropertySourceFactory.class)
21+
public class SparteoConfiguration {
22+
23+
private static final String BIDDER_NAME = "sparteo";
24+
25+
@Bean("sparteoConfigurationProperties")
26+
@ConfigurationProperties("adapters.sparteo")
27+
public BidderConfigurationProperties configurationProperties() {
28+
return new BidderConfigurationProperties();
29+
}
30+
31+
@Bean
32+
public BidderDeps sparteoBidderDeps(BidderConfigurationProperties sparteoConfigurationProperties,
33+
@NotBlank @Value("${external-url}") String externalUrl,
34+
JacksonMapper mapper) {
35+
36+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
37+
.withConfig(sparteoConfigurationProperties)
38+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
39+
.bidderCreator(config -> new SparteoBidder(config.getEndpoint(), mapper))
40+
.assemble();
41+
}
42+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
adapters:
2+
sparteo:
3+
endpoint: https://bid.sparteo.com/s2s-auction
4+
meta-info:
5+
maintainer-email: [email protected]
6+
app-media-types:
7+
- banner
8+
- video
9+
- native
10+
site-media-types:
11+
- banner
12+
- video
13+
- native
14+
supported-vendors:
15+
vendor-id: 1028
16+
usersync:
17+
cookie-family-name: sparteo
18+
iframe:
19+
url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect_url={{redirect_url}}"
20+
support-cors: true
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Sparteo Params",
4+
"type": "object",
5+
"properties": {
6+
"networkId": {
7+
"type": "string",
8+
"description": "Sparteo network ID. This information will be given to you by the Sparteo team."
9+
},
10+
"custom1": {
11+
"type": "string",
12+
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
13+
},
14+
"custom2": {
15+
"type": "string",
16+
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
17+
},
18+
"custom3": {
19+
"type": "string",
20+
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
21+
},
22+
"custom4": {
23+
"type": "string",
24+
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
25+
},
26+
"custom5": {
27+
"type": "string",
28+
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
29+
}
30+
},
31+
"required": [
32+
"networkId"
33+
]
34+
}

0 commit comments

Comments
 (0)