Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Beintoo bidder #813

Merged
merged 9 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions src/main/java/org/prebid/server/bidder/beintoo/BeintooBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package org.prebid.server.bidder.beintoo;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
import com.iab.openrtb.request.Format;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpMethod;
import org.apache.commons.lang3.BooleanUtils;
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.BidderError;
import org.prebid.server.bidder.model.HttpCall;
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.beintoo.ExtImpBeintoo;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;
import org.springframework.util.CollectionUtils;
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class BeintooBidder implements Bidder<BidRequest> {

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

private static final String DEFAULT_BID_CURRENCY = "USD";

private final String endpointUrl;
private final JacksonMapper mapper;

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

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final BidRequest updatedBidRequest;
try {
updatedBidRequest = updateBidRequest(request);
} catch (PreBidException e) {
return Result.emptyWithError(BidderError.badInput(e.getMessage()));
}

final String body = mapper.encode(updatedBidRequest);
final MultiMap headers = makeHeaders(request);
final List<BidderError> errors = new ArrayList<>();
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved

return Result.of(Collections.singletonList(
HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUrl)
.body(body)
.headers(headers)
.payload(request)
.build()), errors);
}

private BidRequest updateBidRequest(BidRequest request) {
final boolean isSecure = isSecure(request.getSite());

final List<Imp> modifiedImps = request.getImp().stream()
.map(imp -> modifyImp(imp, isSecure, parseAndValidateImpExt(imp)))
.collect(Collectors.toList());

return request.toBuilder()
.imp(modifiedImps)
.build();
}

private static boolean isSecure(Site site) {
return site != null && StringUtils.isNotBlank(site.getPage()) && site.getPage().startsWith("https");
}

private ExtImpBeintoo parseAndValidateImpExt(Imp imp) {
final ExtImpBeintoo extImpBeintoo;
try {
extImpBeintoo = mapper.mapper().convertValue(imp.getExt(), BEINTOO_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(e.getMessage(), e);
}

final int tagidNumber;
final String tagId = extImpBeintoo.getTagId();
if (StringUtils.isNumeric(tagId)) {
tagidNumber = Integer.parseInt(tagId);
} else {
throw new PreBidException(String
.format("tagid must be a String of numbers, ignoring imp id=%s", imp.getId()));
}

if (tagidNumber == 0) {
throw new PreBidException(String.format("tagid cant be 0, ignoring imp id=%s",
imp.getId()));
}

return extImpBeintoo;
}

private static Imp modifyImp(Imp imp, boolean isSecure, ExtImpBeintoo extImpBeintoo) {
final Banner banner = modifyImpBanner(imp.getBanner());

final Imp.ImpBuilder impBuilder = imp.toBuilder()
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved
.tagid(extImpBeintoo.getTagId())
.secure(BooleanUtils.toInteger(isSecure))
.banner(banner)
.ext(null);

final String stringBidfloor = extImpBeintoo.getBidFloor();
final BigDecimal bidfloor = StringUtils.isBlank(stringBidfloor) ? null : new BigDecimal(stringBidfloor);
return (bidfloor != null ? bidfloor.compareTo(BigDecimal.ZERO) : 0) > 0
? impBuilder.bidfloor(bidfloor).build()
: impBuilder.build();
}

private static Banner modifyImpBanner(Banner banner) {
if (banner == null) {
throw new PreBidException("Request needs to include a Banner object");
}

if (banner.getW() == null && banner.getH() == null) {
final Banner.BannerBuilder bannerBuilder = banner.toBuilder();
final List<Format> originalFormat = banner.getFormat();

if (CollectionUtils.isEmpty(originalFormat)) {
throw new PreBidException("Need at least one size to build request");
}

final List<Format> formatSkipFirst = originalFormat.subList(1, originalFormat.size());
bannerBuilder.format(formatSkipFirst);

final Format firstFormat = originalFormat.get(0);
bannerBuilder.w(firstFormat.getW());
AndriyPavlyuk marked this conversation as resolved.
Show resolved Hide resolved
bannerBuilder.h(firstFormat.getH());

return bannerBuilder.build();
}

return banner;
}

private static MultiMap makeHeaders(BidRequest request) {
final MultiMap headers = HttpUtil.headers();

final Device device = request.getDevice();
if (device != null) {
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER,
device.getUa());
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER,
device.getIp());
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER,
device.getLanguage());
if (device.getDnt() != null) {
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.DNT_HEADER,
String.valueOf(device.getDnt()));
}
}

final Site site = request.getSite();
if (site != null && StringUtils.isNotBlank(site.getPage())) {
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage());
}

return headers;
}

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

private static List<BidderBid> extractBids(BidResponse bidResponse) {
return bidResponse == null || bidResponse.getSeatbid() == null
? Collections.emptyList()
: bidsFromResponse(bidResponse);
}

private static List<BidderBid> bidsFromResponse(BidResponse bidResponse) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.map(bid -> bid.toBuilder().impid(bid.getId()).build())
.map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur()))
.collect(Collectors.toList());
}

@Override
public Map<String, String> extractTargeting(ObjectNode ext) {
return Collections.emptyMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.prebid.server.proto.openrtb.ext.request.beintoo;

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

@Value
@AllArgsConstructor(staticName = "of")
public class ExtImpBeintoo {

@JsonProperty("tagid")
String tagId;

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

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.beintoo.BeintooBidder;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
import org.prebid.server.spring.config.bidder.model.UsersyncConfigurationProperties;
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
import org.prebid.server.spring.config.bidder.util.BidderInfoCreator;
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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 javax.validation.constraints.NotBlank;

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

private static final String BIDDER_NAME = "beintoo";

@Value("${external-url}")
@NotBlank
private String externalUrl;

@Autowired
private JacksonMapper mapper;

@Autowired
@Qualifier("beintooConfigurationProperties")
private BidderConfigurationProperties configProperties;

@Bean("beintooConfigurationProperties")
@ConfigurationProperties("adapters.beintoo")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps beintooBidderDeps() {
final UsersyncConfigurationProperties usersync = configProperties.getUsersync();

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(configProperties)
.bidderInfo(BidderInfoCreator.create(configProperties))
.usersyncerCreator(UsersyncerCreator.create(usersync, externalUrl))
.bidderCreator(() -> new BeintooBidder(configProperties.getEndpoint(), mapper))
.assemble();
}
}
22 changes: 22 additions & 0 deletions src/main/resources/bidder-config/beintoo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
adapters:
beintoo:
enabled: false
endpoint: https://ib.beintoo.com/um
pbs-enforces-gdpr: true
pbs-enforces-ccpa: true
modifying-vast-xml-allowed: true
deprecated-names:
aliases:
meta-info:
maintainer-email: adops@beintoo.com
app-media-types:
site-media-types:
- banner
supported-vendors:
vendor-id: 618
usersync:
url: https://ib.beintoo.com/um?ssp=pbs&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect=
redirect-url: /setuid?bidder=beintoo&uid=$UID
cookie-family-name: beintoo
type: iframe
support-cors: false
18 changes: 18 additions & 0 deletions src/main/resources/static/bidder-params/beintoo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Beintoo Adapter Params",
"description": "A schema which validates params accepted by the Beintoo adapter",
"type": "object",
"properties": {
"tagid" : {
"type": "string",
"description": "The id of an inventory target"
},
"bidfloor": {
"type": "string",
"description": "The minimum price acceptable for a bid"
}
},

"required": ["tagid"]
}
Loading