Skip to content

Commit

Permalink
TheTradeDesk: Add Bidder (#3370)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Aug 13, 2024
1 parent f486ca5 commit 344cb98
Show file tree
Hide file tree
Showing 12 changed files with 944 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package org.prebid.server.bidder.thetradedesk;

import com.fasterxml.jackson.core.type.TypeReference;
import com.iab.openrtb.request.App;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Format;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.MultiMap;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderCall;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.Result;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.json.DecodeException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

public class TheTradeDeskBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpTheTradeDesk>> TYPE_REFERENCE =
new TypeReference<>() {
};

private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type";
private static final String PREBID_INTEGRATION_TYPE = "1";
private static final MultiMap HEADERS = HttpUtil.headers()
.add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE);

private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$");

private final String endpointUrl;
private final String supplyId;
private final JacksonMapper mapper;

public TheTradeDeskBidder(String endpointUrl, JacksonMapper mapper, String supplyId) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.supplyId = validateSupplyId(supplyId);
this.mapper = Objects.requireNonNull(mapper);
}

private static String validateSupplyId(String supplyId) {
if (StringUtils.isBlank(supplyId) || SUPPLY_ID_PATTERN.matcher(supplyId).matches()) {
return supplyId;
}

throw new IllegalArgumentException("SupplyId must be a simple string provided by TheTradeDesk");
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final List<Imp> modifiedImps = new ArrayList<>();

String publisherId = null;
for (Imp imp : request.getImp()) {
try {
final ExtImpTheTradeDesk extImp = parseImpExt(imp);
publisherId = publisherId == null
? StringUtils.isNotBlank(extImp.getPublisherId())
? extImp.getPublisherId()
: publisherId
: publisherId;

modifiedImps.add(modifyImp(imp));
} catch (PreBidException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
}
}

final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId);
final HttpRequest<BidRequest> httpRequest = BidderUtil.defaultRequest(
outgoingRequest,
HEADERS,
resolveEndpoint(),
mapper);

return Result.withValue(httpRequest);
}

private ExtImpTheTradeDesk parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage(), e);
}
}

private static Imp modifyImp(Imp imp) {
final Banner banner = imp.getBanner();

if (banner != null && CollectionUtils.isNotEmpty(banner.getFormat())) {
final Format format = banner.getFormat().getFirst();
return imp.toBuilder()
.banner(banner.toBuilder().w(format.getW()).h(format.getH()).build())
.build();
}

return imp;
}

private static BidRequest modifyRequest(BidRequest request, List<Imp> modifiedImps, String publisherId) {
return request.toBuilder()
.imp(modifiedImps)
.site(modifySite(request, publisherId))
.app(modifyApp(request, publisherId))
.build();
}

private static Site modifySite(BidRequest request, String publisherId) {
final Site site = request.getSite();
if (site == null) {
return null;
}

return site.toBuilder()
.publisher(modifyPublisher(site.getPublisher(), publisherId))
.build();
}

private static Publisher modifyPublisher(Publisher publisher, String publisherId) {
if (publisher == null) {
return Publisher.builder().id(publisherId).build();
}

return publisher.toBuilder()
.id(StringUtils.isNotBlank(publisherId) ? publisherId : publisher.getId())
.build();
}

private static App modifyApp(BidRequest request, String publisherId) {
final Site site = request.getSite();
final App app = request.getApp();

if (site != null) {
return app;
}

if (app == null) {
return null;
}

return app.toBuilder()
.publisher(modifyPublisher(app.getPublisher(), publisherId))
.build();
}

private String resolveEndpoint() {
return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId)));
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.withValues(extractBids(bidResponse));
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private static List<BidderBid> extractBids(BidResponse bidResponse) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}

return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid).filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
.toList();
}

private static BidType getBidType(Bid bid) {
return switch (bid.getMtype()) {
case 1 -> BidType.banner;
case 2 -> BidType.video;
case 4 -> BidType.xNative;
case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype()));
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.prebid.server.proto.openrtb.ext.request.thetradedesk;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value(staticConstructor = "of")
public class ExtImpTheTradeDesk {

@JsonProperty("publisherId")
String publisherId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.prebid.server.spring.config.bidder;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.thetradedesk.TheTradeDeskBidder;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import jakarta.validation.constraints.NotBlank;

@Configuration
@PropertySource(value = "classpath:/bidder-config/thetradedesk.yaml", factory = YamlPropertySourceFactory.class)
public class TheTradeDeskConfiguration {

private static final String BIDDER_NAME = "thetradedesk";

@Bean("thetradedeskConfigurationProperties")
@ConfigurationProperties("adapters.thetradedesk")
TheTradeDeskConfigurationProperties configurationProperties() {
return new TheTradeDeskConfigurationProperties();
}

@Bean
BidderDeps theTradeDeskBidderDeps(TheTradeDeskConfigurationProperties theTradeDeskConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.<TheTradeDeskConfigurationProperties>forBidder(BIDDER_NAME)
.withConfig(theTradeDeskConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new TheTradeDeskBidder(
config.getEndpoint(),
mapper,
config.getExtraInfo().getSupplyId())
).assemble();
}

@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
private static class TheTradeDeskConfigurationProperties extends BidderConfigurationProperties {

private ExtraInfo extraInfo = new ExtraInfo();
}

@Data
@NoArgsConstructor
private static class ExtraInfo {

String supplyId;
}
}
15 changes: 15 additions & 0 deletions src/main/resources/bidder-config/thetradedesk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
adapters:
thetradedesk:
endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}}
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 21
15 changes: 15 additions & 0 deletions src/main/resources/static/bidder-params/thetradedesk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "The Trade Desk Adapter Params",
"description": "A schema which validates params accepted by the The Trade Desk adapter",
"type": "object",
"properties": {
"publisherId": {
"type": "string",
"description": "An ID which identifies the publisher"
}
},
"required": [
"publisherId"
]
}
Loading

0 comments on commit 344cb98

Please sign in to comment.