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

Invibes bidder/development #957

Merged
merged 6 commits into from
Nov 13, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
325 changes: 325 additions & 0 deletions src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
package org.prebid.server.bidder.invibes;

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.Regs;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.User;
import io.netty.handler.codec.http.HttpResponseStatus;
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.apache.commons.lang3.math.NumberUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.invibes.model.InvibesBidParams;
import org.prebid.server.bidder.invibes.model.InvibesBidRequest;
import org.prebid.server.bidder.invibes.model.InvibesBidderResponse;
import org.prebid.server.bidder.invibes.model.InvibesInternalParams;
import org.prebid.server.bidder.invibes.model.InvibesPlacementProperty;
import org.prebid.server.bidder.invibes.model.InvibesTypedBid;
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.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtUser;
import org.prebid.server.proto.openrtb.ext.request.invibes.ExtImpInvibes;
import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public class InvibesBidder implements Bidder<InvibesBidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpInvibes>> INVIBES_EXT_TYPE_REFERENCE =
new TypeReference<ExtPrebid<?, ExtImpInvibes>>() {
};
private static final String INVIBES_BID_VERSION = "4";
private static final String ADAPTER_VERSION = "prebid_1.0.0";
private static final String URL_HOST_MACRO = "{{Host}}";

private final String endpointUrl;
private final JacksonMapper mapper;

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

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

final String consentString = resolveConsentString(request.getUser());
final Boolean gdprApplies = resolveGDPRApplies(request.getRegs());

InvibesInternalParams invibesInternalParams = new InvibesInternalParams();
invibesInternalParams.setBidParams(InvibesBidParams.builder()
.properties(new HashMap<>())
.placementIds(new ArrayList<>())
.bidVersion(INVIBES_BID_VERSION)
.build());

for (Imp imp : request.getImp()) {
final ExtImpInvibes extImpInvibes;
try {
extImpInvibes = parseImpExt(imp);
validateImp(imp);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
continue;
}

invibesInternalParams = updateInvibesInternalParams(invibesInternalParams, extImpInvibes, imp);
}
//TODO add AMP parameter to invibesInternalParams, after reqInfo will be implemented

final List<String> placementIds = invibesInternalParams.getBidParams().getPlacementIds();
if (CollectionUtils.isEmpty(placementIds)) {
return Result.of(Collections.emptyList(), errors);
}

invibesInternalParams.setGdpr(gdprApplies);
invibesInternalParams.setGdprConsent(consentString);

try {
if (request.getSite() == null) {
throw new PreBidException("Site not specified");
}
final HttpRequest<InvibesBidRequest> httpRequest = makeRequest(invibesInternalParams, request);
return Result.of(Collections.singletonList(httpRequest), errors);
} catch (PreBidException e) {
return Result.emptyWithError(BidderError.badInput(e.getMessage()));
}
}

private ExtImpInvibes parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), INVIBES_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException("Error parsing invibesExt parameters");
}
}

private void validateImp(Imp imp) {
if (imp.getBanner() == null) {
throw new PreBidException("Banner not specified");
}
}

private String resolveConsentString(User user) {
final ExtUser extUser = user != null ? user.getExt() : null;
return extUser != null ? extUser.getConsent() : StringUtils.EMPTY;
}

private Boolean resolveGDPRApplies(Regs regs) {
final ExtRegs extRegs = regs != null ? regs.getExt() : null;
final Integer gdpr = extRegs != null ? extRegs.getGdpr() : null;

return gdpr != null ? gdpr == 1 : Boolean.TRUE;
}

private InvibesInternalParams updateInvibesInternalParams(InvibesInternalParams invibesInternalParams,
ExtImpInvibes invibesExt,
Imp imp) {
final String impExtPlacementId = invibesExt.getPlacementId();
final InvibesBidParams bidParams = invibesInternalParams.getBidParams();
final List<String> updatedPlacementIds = bidParams.getPlacementIds();

if (StringUtils.isNotBlank(impExtPlacementId)) {
updatedPlacementIds.add(impExtPlacementId.trim());
}
final Banner banner = imp.getBanner();
final List<Format> adFormats = resolveAdFormats(banner);

bidParams.getProperties()
.put(impExtPlacementId, InvibesPlacementProperty.builder()
.impId(imp.getId())
.formats(adFormats)
.build());

final InvibesBidParams updatedBidParams = bidParams.toBuilder()
.placementIds(updatedPlacementIds)
.properties(bidParams.getProperties())
SerhiiNahornyi marked this conversation as resolved.
Show resolved Hide resolved
.build();

invibesInternalParams.setDomainId(invibesExt.getDomainId());
invibesInternalParams.setBidParams(updatedBidParams);

final InvibesDebug invibesDebug = invibesExt.getDebug();
final String invibesDebugTestBvid = invibesDebug != null ? invibesDebug.getTestBvid() : null;
if (StringUtils.isNotBlank(invibesDebugTestBvid)) {
invibesInternalParams.setTestBvid(invibesDebugTestBvid);
}

if (invibesDebug != null) {
invibesInternalParams.setTestLog(invibesDebug.getTestLog());
}

return invibesInternalParams;
}

private List<Format> resolveAdFormats(Banner currentBanner) {
if (currentBanner.getFormat() != null) {
return currentBanner.getFormat();
} else {
final Integer formatW = currentBanner.getW();
final Integer formatH = currentBanner.getH();
return formatW != null && formatH != null
? Collections.singletonList(Format.builder().w(formatW).h(formatH).build())
: Collections.emptyList();
}
}

private HttpRequest<InvibesBidRequest> makeRequest(InvibesInternalParams invibesParams,
BidRequest request) {
final String host = resolveHost(invibesParams.getDomainId());
final String url = endpointUrl.replace(URL_HOST_MACRO, host);
final InvibesBidRequest parameter = resolveParameter(invibesParams, request);

final Device device = request.getDevice();
final Site site = request.getSite();
final MultiMap headers = resolveHeaders(device, site);

final String body = mapper.encode(parameter);

return HttpRequest.<InvibesBidRequest>builder()
.method(HttpMethod.POST)
.uri(url)
.headers(headers)
.payload(parameter)
.body(body)
.build();
}

private InvibesBidRequest resolveParameter(InvibesInternalParams invibesParams, BidRequest request) {
final User user = request.getUser();
final String buyeruid = user != null ? user.getBuyeruid() : null;
final String lid = StringUtils.isNotBlank(buyeruid) ? buyeruid : StringUtils.EMPTY;

return createRequest(invibesParams, lid, request.getDevice(), request.getSite());
}

private InvibesBidRequest createRequest(InvibesInternalParams invibesParams, String lid,
Device device, Site site) {
final String testBvid = invibesParams.getTestBvid();
final Boolean testLog = invibesParams.getTestLog();

return InvibesBidRequest.builder()
.isTestBid(StringUtils.isNotBlank(testBvid))
.bidParamsJson(mapper.encode(invibesParams.getBidParams()))
SerhiiNahornyi marked this conversation as resolved.
Show resolved Hide resolved
.location(site.getPage())
.lid(lid)
.kw(site.getKeywords())
.isAmp(invibesParams.getIsAmp())
.width(resolveWidth(device))
.height(resolveHeight(device))
.gdprConsent(invibesParams.getGdprConsent())
.gdpr(invibesParams.getGdpr())
.bvid(testBvid)
.invibBVLog(testLog)
.videoAdDebug(testLog)
.build();
}

private static String resolveHeight(Device device) {
final Integer height = device != null ? device.getH() : null;

return height != null && height > NumberUtils.INTEGER_ZERO ? height.toString() : null;
}

private static String resolveWidth(Device device) {
final Integer width = device != null ? device.getW() : null;

return width != null && width > NumberUtils.INTEGER_ZERO ? width.toString() : null;
}

private String resolveHost(Integer domainId) {
if (domainId == null) {
return "bid.videostep.com";
} else if (domainId >= 1002) {
return String.format("bid%s.videostep.com", domainId - 1000);
} else if (domainId == 1) {
return "adweb.videostepstage.com";
} else if (domainId == 2) {
return "adweb.invibesstage.com";
} else {
return "bid.videostep.com";
}
}

private MultiMap resolveHeaders(Device device, Site site) {
final MultiMap headers = HttpUtil.headers();
if (device != null) {
if (StringUtils.isNotBlank(device.getIp())) {
headers.add("X-Forwarded-For", device.getIp());
} else if (StringUtils.isNotBlank(device.getIpv6())) {
headers.add("X-Forwarded-For", device.getIpv6());
}
}
if (site != null) {
headers.add("Referer", site.getPage());
}
headers.add("Aver", ADAPTER_VERSION);
return headers;
}

@Override
public final Result<List<BidderBid>> makeBids(HttpCall<InvibesBidRequest> httpCall, BidRequest bidRequest) {
final int statusCode = httpCall.getResponse().getStatusCode();
if (statusCode == HttpResponseStatus.NO_CONTENT.code()) {
return Result.of(Collections.emptyList(), Collections.emptyList());
}

try {
final InvibesBidderResponse bidResponse =
mapper.decodeValue(httpCall.getResponse().getBody(), InvibesBidderResponse.class);
if (bidResponse != null && StringUtils.isNotBlank(bidResponse.getError())) {
return Result.emptyWithError(
BidderError.badServerResponse(String.format("Server error: %s.", bidResponse.getError())));
}
return Result.of(extractBids(bidResponse), Collections.emptyList());
} catch (DecodeException | PreBidException e) {
return Result.emptyWithError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(InvibesBidderResponse bidResponse) {
return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getTypedBids())
? Collections.emptyList()
: bidsFromResponse(bidResponse);
}

private List<BidderBid> bidsFromResponse(InvibesBidderResponse bidResponse) {
return bidResponse.getTypedBids().stream()
.filter(Objects::nonNull)
.map(InvibesTypedBid::getBid)
.filter(Objects::nonNull)
//TODO add DealPriority
.map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCurrency()))
.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,22 @@
package org.prebid.server.bidder.invibes.model;

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

import java.util.List;
import java.util.Map;

@Builder(toBuilder = true)
@Value
public class InvibesBidParams {

@JsonProperty("PlacementIds")
List<String> placementIds;

@JsonProperty("BidVersion")
String bidVersion;

@JsonProperty("Properties")
Map<String, InvibesPlacementProperty> properties;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.prebid.server.bidder.invibes.model;

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

@Builder(toBuilder = true)
@Value
public class InvibesBidRequest {

@JsonProperty("BidParamsJson")
String bidParamsJson;

@JsonProperty("Location")
String location;

@JsonProperty("Lid")
String lid;

@JsonProperty("IsTestBid")
Boolean isTestBid;

@JsonProperty("Kw")
String kw;

@JsonProperty("IsAMP")
Boolean isAmp;

@JsonProperty("Width")
String width;

@JsonProperty("Height")
String height;

@JsonProperty("GdprConsent")
String gdprConsent;

@JsonProperty("Gdpr")
Boolean gdpr;

@JsonProperty("Bvid")
String bvid;

@JsonProperty("InvibBVLog")
Boolean invibBVLog;

@JsonProperty("VideoAdDebug")
Boolean videoAdDebug;
}
Loading