Skip to content

Commit 70b88dd

Browse files
committed
New Adapter: Sparteo
1 parent 1e66556 commit 70b88dd

File tree

12 files changed

+1550
-0
lines changed

12 files changed

+1550
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package org.prebid.server.bidder.sparteo;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
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.request.Publisher;
9+
import com.iab.openrtb.request.Site;
10+
import com.iab.openrtb.response.Bid;
11+
import com.iab.openrtb.response.BidResponse;
12+
import com.iab.openrtb.response.SeatBid;
13+
import io.vertx.core.http.HttpMethod;
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.json.DecodeException;
22+
import org.prebid.server.json.JacksonMapper;
23+
import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
24+
import org.prebid.server.proto.openrtb.ext.response.BidType;
25+
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
26+
import org.prebid.server.util.BidderUtil;
27+
import org.prebid.server.util.HttpUtil;
28+
29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.Iterator;
32+
import java.util.List;
33+
import java.util.Objects;
34+
35+
public class SparteoBidder implements Bidder<BidRequest> {
36+
37+
private final String endpointUrl;
38+
private final JacksonMapper mapper;
39+
40+
public SparteoBidder(String endpointUrl, JacksonMapper mapper) {
41+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
42+
this.mapper = Objects.requireNonNull(mapper);
43+
}
44+
45+
private static <T> Iterable<T> iterable(Iterator<T> it) {
46+
return () -> it;
47+
}
48+
49+
@Override
50+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
51+
final List<BidderError> errors = new ArrayList<>();
52+
53+
String siteNetworkId = null;
54+
final List<Imp> modifiedImps = new ArrayList<>();
55+
56+
for (Imp imp : request.getImp()) {
57+
try {
58+
final ObjectNode extMap = mapper.mapper()
59+
.convertValue(imp.getExt(), ObjectNode.class);
60+
61+
final ObjectNode bidderNode = (ObjectNode) extMap.remove("bidder");
62+
63+
if (bidderNode != null) {
64+
if (siteNetworkId == null && bidderNode.hasNonNull("networkId")) {
65+
siteNetworkId = bidderNode.get("networkId").asText();
66+
}
67+
68+
final ObjectNode sparteoNode = extMap.has("sparteo") && extMap.get("sparteo").isObject()
69+
? (ObjectNode) extMap.get("sparteo")
70+
: extMap.putObject("sparteo");
71+
final ObjectNode paramsNode = sparteoNode.has("params") && sparteoNode.get("params").isObject()
72+
? (ObjectNode) sparteoNode.get("params")
73+
: sparteoNode.putObject("params");
74+
75+
for (String field : iterable(bidderNode.fieldNames())) {
76+
paramsNode.set(field, bidderNode.get(field));
77+
}
78+
}
79+
80+
modifiedImps.add(imp.toBuilder().ext(extMap).build());
81+
} catch (Exception e) {
82+
errors.add(BidderError.badInput(
83+
String.format("ignoring imp id=%s, error processing ext: %s",
84+
imp.getId(), e.getMessage())));
85+
}
86+
}
87+
88+
if (modifiedImps.isEmpty()) {
89+
return Result.withErrors(errors);
90+
}
91+
92+
final BidRequest.BidRequestBuilder rb = request.toBuilder().imp(modifiedImps);
93+
94+
final Site site = request.getSite();
95+
if (site != null && site.getPublisher() != null && siteNetworkId != null) {
96+
final Publisher pub = site.getPublisher();
97+
98+
final ObjectNode pubExtRaw = pub.getExt() != null
99+
? mapper.mapper().convertValue(pub.getExt(), ObjectNode.class)
100+
: mapper.mapper().createObjectNode();
101+
102+
pubExtRaw.withObjectProperty("params").put("networkId", siteNetworkId);
103+
104+
final ExtPublisher extPub = mapper.mapper()
105+
.convertValue(pubExtRaw, ExtPublisher.class);
106+
107+
final Publisher newPub = pub.toBuilder().ext(extPub).build();
108+
final Site newSite = site.toBuilder().publisher(newPub).build();
109+
rb.site(newSite);
110+
}
111+
112+
final BidRequest outgoing = rb.build();
113+
114+
final HttpRequest<BidRequest> call = HttpRequest.<BidRequest>builder()
115+
.method(HttpMethod.POST)
116+
.uri(endpointUrl)
117+
.headers(HttpUtil.headers())
118+
.impIds(BidderUtil.impIds(outgoing))
119+
.body(mapper.encodeToBytes(outgoing))
120+
.payload(outgoing)
121+
.build();
122+
123+
final List<HttpRequest<BidRequest>> calls = Collections.singletonList(call);
124+
125+
return errors.isEmpty()
126+
? Result.withValues(calls)
127+
: Result.of(calls, errors);
128+
}
129+
130+
private BidType getBidTypeFromBidExtension(Bid bid) throws Exception {
131+
final ObjectNode bidExtNode = bid.getExt();
132+
133+
if (bidExtNode == null || !bidExtNode.hasNonNull("prebid")) {
134+
throw new Exception(
135+
String.format("Bid extension or bid.ext.prebid missing for impression id: %s",
136+
bid.getImpid())
137+
);
138+
}
139+
140+
final JsonNode prebidNode = bidExtNode.get("prebid");
141+
final ExtBidPrebid extBidPrebid;
142+
143+
try {
144+
extBidPrebid = mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class);
145+
} catch (JsonProcessingException e) {
146+
throw new Exception(
147+
String.format("Failed to parse bid.ext.prebid for impression id: %s, error: %s",
148+
bid.getImpid(),
149+
e.getMessage()
150+
),
151+
e);
152+
}
153+
154+
if (extBidPrebid == null || extBidPrebid.getType() == null) {
155+
throw new Exception(
156+
String.format("Missing type in bid.ext.prebid for impression id: %s",
157+
bid.getImpid()
158+
));
159+
}
160+
161+
final BidType bidType = extBidPrebid.getType();
162+
if (bidType == BidType.audio) {
163+
throw new Exception(
164+
String.format("Audio bid type not supported by this adapter for impression id: %s",
165+
bid.getImpid())
166+
);
167+
}
168+
169+
return bidType;
170+
}
171+
172+
@Override
173+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
174+
final List<BidderError> errors = new ArrayList<>();
175+
176+
final int status = httpCall.getResponse().getStatusCode();
177+
if (status == 204) {
178+
return Result.of(Collections.emptyList(), errors);
179+
}
180+
if (status != 200) {
181+
errors.add(BidderError.badServerResponse(
182+
String.format("HTTP status %d returned from Sparteo", status))
183+
);
184+
return Result.of(Collections.emptyList(), errors);
185+
}
186+
187+
final BidResponse bidResponse;
188+
try {
189+
bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
190+
} catch (DecodeException e) {
191+
errors.add(BidderError.badServerResponse(
192+
String.format("Failed to decode Sparteo response: %s", e.getMessage()))
193+
);
194+
return Result.of(Collections.emptyList(), errors);
195+
}
196+
197+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
198+
return Result.of(Collections.emptyList(), errors);
199+
}
200+
201+
final List<BidderBid> bidderBids = new ArrayList<>();
202+
final String currency = bidResponse.getCur();
203+
204+
for (SeatBid seatBid : bidResponse.getSeatbid()) {
205+
if (seatBid != null && CollectionUtils.isNotEmpty(seatBid.getBid())) {
206+
for (Bid bid : seatBid.getBid()) {
207+
if (bid == null) {
208+
errors.add(BidderError.badServerResponse(
209+
"Received null bid object within a seatbid.")
210+
);
211+
continue;
212+
}
213+
try {
214+
final BidType type = getBidTypeFromBidExtension(bid);
215+
bidderBids.add(BidderBid.of(bid, type, currency));
216+
} catch (Exception e) {
217+
errors.add(BidderError.badServerResponse(e.getMessage()));
218+
}
219+
}
220+
}
221+
}
222+
223+
return Result.of(bidderBids, errors);
224+
}
225+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.prebid.server.proto.openrtb.ext.request.sparteo;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import lombok.NonNull;
6+
import lombok.Value;
7+
8+
@Value(staticConstructor = "of")
9+
@JsonInclude(JsonInclude.Include.NON_NULL)
10+
public class ExtImpSparteo {
11+
12+
@NonNull
13+
@JsonProperty("networkId")
14+
String networkId;
15+
16+
@JsonProperty("custom1")
17+
String custom1;
18+
19+
@JsonProperty("custom2")
20+
String custom2;
21+
22+
@JsonProperty("custom3")
23+
String custom3;
24+
25+
@JsonProperty("custom4")
26+
String custom4;
27+
28+
@JsonProperty("custom5")
29+
String custom5;
30+
}
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+
supportCors: true
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
"additionalProperties": true
35+
}

0 commit comments

Comments
 (0)