Skip to content

Commit 28ae7cf

Browse files
authored
MinuteMedia: Add new bidder (prebid#3019)
1 parent d47d447 commit 28ae7cf

12 files changed

Lines changed: 796 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package org.prebid.server.bidder.minutemedia;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.iab.openrtb.request.BidRequest;
5+
import com.iab.openrtb.request.Imp;
6+
import com.iab.openrtb.response.Bid;
7+
import com.iab.openrtb.response.BidResponse;
8+
import com.iab.openrtb.response.SeatBid;
9+
import org.apache.commons.collections4.CollectionUtils;
10+
import org.apache.commons.lang3.StringUtils;
11+
import org.prebid.server.bidder.Bidder;
12+
import org.prebid.server.bidder.model.BidderBid;
13+
import org.prebid.server.bidder.model.BidderCall;
14+
import org.prebid.server.bidder.model.BidderError;
15+
import org.prebid.server.bidder.model.HttpRequest;
16+
import org.prebid.server.bidder.model.Result;
17+
import org.prebid.server.exception.PreBidException;
18+
import org.prebid.server.json.DecodeException;
19+
import org.prebid.server.json.JacksonMapper;
20+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
21+
import org.prebid.server.proto.openrtb.ext.request.minutemedia.ExtImpMinuteMedia;
22+
import org.prebid.server.proto.openrtb.ext.response.BidType;
23+
import org.prebid.server.util.BidderUtil;
24+
import org.prebid.server.util.HttpUtil;
25+
26+
import java.util.ArrayList;
27+
import java.util.Collection;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.Objects;
31+
32+
public class MinuteMediaBidder implements Bidder<BidRequest> {
33+
34+
private static final TypeReference<ExtPrebid<?, ExtImpMinuteMedia>> MINUTE_MEDIA_EXT_TYPE_REFERENCE =
35+
new TypeReference<>() {
36+
};
37+
public static final String PUBLISHER_ID_MACRO = "{{PublisherId}}";
38+
39+
private final String endpointUrl;
40+
private final JacksonMapper mapper;
41+
42+
public MinuteMediaBidder(String endpointUrl, JacksonMapper mapper) {
43+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
44+
this.mapper = Objects.requireNonNull(mapper);
45+
}
46+
47+
@Override
48+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
49+
final String orgId;
50+
51+
try {
52+
orgId = extractFirstImpOrdId(bidRequest.getImp());
53+
} catch (PreBidException e) {
54+
return Result.withError(BidderError.badInput(e.getMessage()));
55+
}
56+
57+
return Result.withValue(BidderUtil.defaultRequest(bidRequest, resolveEndpoint(endpointUrl, orgId), mapper));
58+
}
59+
60+
private String extractFirstImpOrdId(List<Imp> imps) {
61+
return imps.stream()
62+
.findFirst()
63+
.map(this::parseImpExt)
64+
.map(ExtImpMinuteMedia::getOrg)
65+
.map(String::strip)
66+
.filter(StringUtils::isNotBlank)
67+
.orElseThrow(() -> new PreBidException(
68+
"Failed to extract bidrequest.imp[0].ext.prebid.bidder.minutemedia.org parameter"));
69+
}
70+
71+
private ExtImpMinuteMedia parseImpExt(Imp imp) {
72+
try {
73+
return mapper.mapper().convertValue(imp.getExt(), MINUTE_MEDIA_EXT_TYPE_REFERENCE).getBidder();
74+
} catch (IllegalArgumentException e) {
75+
throw new PreBidException(e.getMessage());
76+
}
77+
}
78+
79+
private String resolveEndpoint(String endpointUrl, String orgId) {
80+
return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(orgId));
81+
}
82+
83+
@Override
84+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
85+
final List<BidderError> errors = new ArrayList<>();
86+
try {
87+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
88+
return Result.of(extractBids(bidResponse, errors), errors);
89+
} catch (DecodeException e) {
90+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
91+
}
92+
}
93+
94+
private static List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
95+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
96+
return Collections.emptyList();
97+
}
98+
return bidsFromResponse(bidResponse, errors);
99+
}
100+
101+
private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<BidderError> errors) {
102+
return bidResponse.getSeatbid().stream()
103+
.filter(Objects::nonNull)
104+
.map(SeatBid::getBid)
105+
.filter(Objects::nonNull)
106+
.flatMap(Collection::stream)
107+
.map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors))
108+
.filter(Objects::nonNull)
109+
.toList();
110+
}
111+
112+
private static BidderBid makeBidderBid(Bid bid, String currency, List<BidderError> errors) {
113+
try {
114+
return BidderBid.of(bid, getBidType(bid), currency);
115+
} catch (PreBidException e) {
116+
errors.add(BidderError.badServerResponse(e.getMessage()));
117+
return null;
118+
}
119+
}
120+
121+
private static BidType getBidType(Bid bid) {
122+
final Integer markupType = bid.getMtype();
123+
if (markupType == null) {
124+
throw new PreBidException("Missing mediaType for bid: %s".formatted(bid.getId()));
125+
}
126+
127+
return switch (markupType) {
128+
case 1 -> BidType.banner;
129+
case 2 -> BidType.video;
130+
default -> throw new PreBidException(
131+
"Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid()));
132+
};
133+
}
134+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.prebid.server.proto.openrtb.ext.request.minutemedia;
2+
3+
import lombok.Value;
4+
5+
@Value(staticConstructor = "of")
6+
public class ExtImpMinuteMedia {
7+
8+
String org;
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import org.prebid.server.bidder.BidderDeps;
4+
import org.prebid.server.bidder.minutemedia.MinuteMediaBidder;
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 javax.validation.constraints.NotBlank;
17+
18+
@Configuration
19+
@PropertySource(value = "classpath:/bidder-config/minutemedia.yaml", factory = YamlPropertySourceFactory.class)
20+
public class MinuteMediaConfiguration {
21+
22+
private static final String BIDDER_NAME = "minutemedia";
23+
24+
@Bean("minutemediaConfigurationProperties")
25+
@ConfigurationProperties("adapters.minutemedia")
26+
BidderConfigurationProperties configurationProperties() {
27+
return new BidderConfigurationProperties();
28+
}
29+
30+
@Bean
31+
BidderDeps minutemediaBidderDeps(BidderConfigurationProperties minutemediaConfigurationProperties,
32+
@NotBlank @Value("${external-url}") String externalUrl,
33+
JacksonMapper mapper) {
34+
35+
return BidderDepsAssembler.forBidder(BIDDER_NAME)
36+
.withConfig(minutemediaConfigurationProperties)
37+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
38+
.bidderCreator(config -> new MinuteMediaBidder(config.getEndpoint(), mapper))
39+
.assemble();
40+
}
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
adapters:
2+
minutemedia:
3+
endpoint: https://pbs.minutemedia-prebid.com/pbs-mm?publisher_id={{PublisherId}}
4+
modifying-vast-xml-allowed: true
5+
meta-info:
6+
maintainer-email: hb@minutemedia.com
7+
app-media-types:
8+
- banner
9+
- video
10+
site-media-types:
11+
- banner
12+
- video
13+
vendor-id: 918
14+
usersync:
15+
cookie-family-name: minutemedia
16+
iframe:
17+
url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}}
18+
support-cors: false
19+
uid-macro: '[PBS_UID]'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "MinuteMedia Adapter Params",
4+
"description": "A schema which validates params accepted by the MinuteMedia adapter",
5+
"type": "object",
6+
"properties": {
7+
"org": {
8+
"type": "string",
9+
"description": "The organization ID.",
10+
"minLength": 1
11+
}
12+
},
13+
"required": [
14+
"org"
15+
]
16+
}

0 commit comments

Comments
 (0)