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

VerizonMedia rebranding into YSSP #1409

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
203 changes: 203 additions & 0 deletions src/main/java/org/prebid/server/bidder/yssp/YsspBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package org.prebid.server.bidder.yssp;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.App;
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.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.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.yssp.ExtImpYssp;
import org.prebid.server.proto.openrtb.ext.response.BidType;
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.stream.Collectors;

public class YsspBidder implements Bidder<BidRequest> {

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

private final String endpointUrl;
private final JacksonMapper mapper;

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

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
final List<HttpRequest<BidRequest>> bidRequests = new ArrayList<>();
final List<BidderError> errors = new ArrayList<>();

final List<Imp> impList = bidRequest.getImp();
for (int i = 0; i < impList.size(); i++) {
try {
final Imp imp = impList.get(i);
final ExtImpYssp extImpYssp = parseAndValidateImpExt(imp.getExt(), i);
final BidRequest modifiedRequest = modifyRequest(bidRequest, imp, extImpYssp);
bidRequests.add(makeHttpRequest(modifiedRequest));
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}

return Result.of(bidRequests, errors);
}

private ExtImpYssp parseAndValidateImpExt(ObjectNode impExtNode, int index) {
final ExtImpYssp extImpYssp;
try {
extImpYssp = mapper.mapper().convertValue(impExtNode, YSSP_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException(String.format("imp #%s: %s", index, e.getMessage()));
}

final String dcn = extImpYssp.getDcn();
if (StringUtils.isBlank(dcn)) {
throw new PreBidException(String.format("imp #%s: missing param dcn", index));
}

final String pos = extImpYssp.getPos();
if (StringUtils.isBlank(pos)) {
throw new PreBidException(String.format("imp #%s: missing param pos", index));
}

return extImpYssp;
}

private static BidRequest modifyRequest(BidRequest request, Imp imp, ExtImpYssp extImpYssp) {
final Banner banner = imp.getBanner();
final boolean hasBanner = banner != null;

final Integer bannerWidth = hasBanner ? banner.getW() : null;
final Integer bannerHeight = hasBanner ? banner.getH() : null;
final boolean hasBannerWidthAndHeight = bannerWidth != null && bannerHeight != null;

if (hasBannerWidthAndHeight && (bannerWidth == 0 || bannerHeight == 0)) {
throw new PreBidException(String.format(
"Invalid sizes provided for Banner %sx%s", bannerWidth, bannerHeight));
}

final Imp.ImpBuilder impBuilder = imp.toBuilder()
.tagid(extImpYssp.getPos());

if (hasBanner && !hasBannerWidthAndHeight) {
impBuilder.banner(modifyBanner(banner));
}

final BidRequest.BidRequestBuilder requestBuilder = request.toBuilder();

final Site site = request.getSite();
final App app = request.getApp();
if (site != null) {
requestBuilder.site(site.toBuilder().id(extImpYssp.getDcn()).build());
} else if (app != null) {
requestBuilder.app(app.toBuilder().id(extImpYssp.getDcn()).build());
}

return requestBuilder
.imp(Collections.singletonList(impBuilder.build()))
.build();
}

private static Banner modifyBanner(Banner banner) {
final List<Format> bannerFormats = banner.getFormat();
if (CollectionUtils.isEmpty(bannerFormats)) {
throw new PreBidException("No sizes provided for Banner");
}
final Format firstFormat = bannerFormats.get(0);

return banner.toBuilder()
.w(firstFormat.getW())
.h(firstFormat.getH())
.build();
}

private HttpRequest<BidRequest> makeHttpRequest(BidRequest outgoingRequest) {
return HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUrl)
.body(mapper.encode(outgoingRequest))
.headers(makeHeaders(outgoingRequest.getDevice()))
.payload(outgoingRequest)
.build();
}

private static MultiMap makeHeaders(Device device) {
final MultiMap headers = HttpUtil.headers()
.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5");

final String deviceUa = device != null ? device.getUa() : null;
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, deviceUa);

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, httpCall.getRequest().getPayload()), Collections.emptyList());
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private static List<BidderBid> extractBids(BidResponse bidResponse, BidRequest bidRequest) {
final List<SeatBid> seatBids = bidResponse != null ? bidResponse.getSeatbid() : null;
if (seatBids == null) {
return Collections.emptyList();
}

if (seatBids.isEmpty()) {
throw new PreBidException(String.format("Invalid SeatBids count: %d", seatBids.size()));
}
return bidsFromResponse(bidResponse, bidRequest.getImp());
}

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

private static boolean checkBid(String bidImpId, List<Imp> imps) {
for (Imp imp : imps) {
if (imp.getId().equals(bidImpId)) {
return imp.getBanner() != null;
}
}
throw new PreBidException(String.format("Unknown ad unit code '%s'", bidImpId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.prebid.server.proto.openrtb.ext.request.yssp;

import lombok.AllArgsConstructor;
import lombok.Value;

/**
* Defines the contract for bidRequest.imp[i].ext.yssp
*/
@AllArgsConstructor(staticName = "of")
@Value
public class ExtImpYssp {

String dcn;

String pos;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.yssp.YsspBidder;
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.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/yssp.yaml", factory = YamlPropertySourceFactory.class)
public class YsspConfiguration {

private static final String BIDDER_NAME = "yssp";

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

@Autowired
private JacksonMapper mapper;

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

@Bean("ysspConfigurationProperties")
@ConfigurationProperties("adapters.yssp")
BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
BidderDeps ysspBidderDeps() {
return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(configProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new YsspBidder(config.getEndpoint(), mapper))
.assemble();
}
}
23 changes: 23 additions & 0 deletions src/main/resources/bidder-config/yssp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
adapters:
yssp:
enabled: false
endpoint: https://s2shb.ssp.yahoo.com/admax/bid/partners/MAG
pbs-enforces-gdpr: true
pbs-enforces-ccpa: true
modifying-vast-xml-allowed: true
deprecated-names:
aliases: {}
meta-info:
maintainer-email: hb-fe-tech@oath.com
app-media-types:
- banner
site-media-types:
- banner
supported-vendors:
vendor-id: 25
usersync:
url: https://ups.analytics.yahoo.com/ups/58401/sync?redir=true&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}
redirect-url:
cookie-family-name: yssp
type: redirect
support-cors: false
20 changes: 20 additions & 0 deletions src/main/resources/static/bidder-params/yssp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "YSSP Adapter Params",
"description": "A schema which validates params accepted by the YSSP adapter",
"type": "object",
"properties": {
"dcn": {
"type": "string",
"description": "Site ID provided by One Mobile"
},
"pos": {
"type": "string",
"description": "Placement ID"
}
},
"required": [
"dcn",
"pos"
]
}
Loading