github.com/prebid/prebid-server@v0.275.0/adapters/consumable/consumable.go (about) 1 package consumable 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strconv" 9 "strings" 10 11 "github.com/prebid/openrtb/v19/openrtb2" 12 "github.com/prebid/prebid-server/adapters" 13 "github.com/prebid/prebid-server/config" 14 "github.com/prebid/prebid-server/errortypes" 15 "github.com/prebid/prebid-server/openrtb_ext" 16 "github.com/prebid/prebid-server/privacy/ccpa" 17 ) 18 19 type ConsumableAdapter struct { 20 clock instant 21 endpoint string 22 } 23 24 type bidRequest struct { 25 Placements []placement `json:"placements"` 26 Time int64 `json:"time"` 27 NetworkId int `json:"networkId,omitempty"` 28 SiteId int `json:"siteId"` 29 UnitId int `json:"unitId"` 30 UnitName string `json:"unitName,omitempty"` 31 IncludePricingData bool `json:"includePricingData"` 32 User user `json:"user,omitempty"` 33 Referrer string `json:"referrer,omitempty"` 34 Ip string `json:"ip,omitempty"` 35 Url string `json:"url,omitempty"` 36 EnableBotFiltering bool `json:"enableBotFiltering,omitempty"` 37 Parallel bool `json:"parallel"` 38 CCPA string `json:"ccpa,omitempty"` 39 GDPR *bidGdpr `json:"gdpr,omitempty"` 40 Coppa bool `json:"coppa,omitempty"` 41 SChain openrtb2.SupplyChain `json:"schain"` 42 Content *openrtb2.Content `json:"content,omitempty"` 43 GPP string `json:"gpp,omitempty"` 44 GPPSID []int8 `json:"gpp_sid,omitempty"` 45 } 46 47 type placement struct { 48 DivName string `json:"divName"` 49 NetworkId int `json:"networkId,omitempty"` 50 SiteId int `json:"siteId"` 51 UnitId int `json:"unitId"` 52 UnitName string `json:"unitName,omitempty"` 53 AdTypes []int `json:"adTypes"` 54 } 55 56 type user struct { 57 Key string `json:"key,omitempty"` 58 Eids []openrtb2.EID `json:"eids,omitempty"` 59 } 60 61 type bidGdpr struct { 62 Applies *bool `json:"applies,omitempty"` 63 Consent string `json:"consent,omitempty"` 64 } 65 66 type bidResponse struct { 67 Decisions map[string]decision `json:"decisions"` // map by bidId 68 } 69 70 /** 71 * See https://dev.adzerk.com/v1.0/reference/response 72 */ 73 type decision struct { 74 Pricing *pricing `json:"pricing"` 75 AdID int64 `json:"adId"` 76 BidderName string `json:"bidderName,omitempty"` 77 CreativeID string `json:"creativeId,omitempty"` 78 Contents []contents `json:"contents"` 79 ImpressionUrl *string `json:"impressionUrl,omitempty"` 80 Width uint64 `json:"width,omitempty"` // Consumable extension, not defined by Adzerk 81 Height uint64 `json:"height,omitempty"` // Consumable extension, not defined by Adzerk 82 Adomain []string `json:"adomain,omitempty"` 83 Cats []string `json:"cats,omitempty"` 84 } 85 86 type contents struct { 87 Body string `json:"body"` 88 } 89 90 type pricing struct { 91 ClearPrice *float64 `json:"clearPrice"` 92 } 93 94 func (a *ConsumableAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 95 var errs []error 96 97 headers := http.Header{ 98 "Content-Type": {"application/json"}, 99 "Accept": {"application/json"}, 100 } 101 102 if request.Device != nil { 103 if request.Device.UA != "" { 104 headers.Set("User-Agent", request.Device.UA) 105 } 106 107 if request.Device.IP != "" { 108 headers.Set("Forwarded", "for="+request.Device.IP) 109 headers.Set("X-Forwarded-For", request.Device.IP) 110 } 111 } 112 113 // Set azk cookie to one we got via sync 114 if request.User != nil { 115 userID := strings.TrimSpace(request.User.BuyerUID) 116 if len(userID) > 0 { 117 headers.Add("Cookie", fmt.Sprintf("%s=%s", "azk", userID)) 118 } 119 } 120 121 if request.Site != nil && request.Site.Page != "" { 122 headers.Set("Referer", request.Site.Page) 123 124 pageUrl, err := url.Parse(request.Site.Page) 125 if err != nil { 126 errs = append(errs, err) 127 } else { 128 origin := url.URL{ 129 Scheme: pageUrl.Scheme, 130 Opaque: pageUrl.Opaque, 131 Host: pageUrl.Host, 132 } 133 headers.Set("Origin", origin.String()) 134 } 135 } 136 137 body := bidRequest{ 138 Placements: make([]placement, len(request.Imp)), 139 Time: a.clock.Now().Unix(), 140 IncludePricingData: true, 141 EnableBotFiltering: true, 142 Parallel: true, 143 } 144 145 if request.Site != nil { 146 body.Referrer = request.Site.Ref // Effectively the previous page to the page where the ad will be shown 147 body.Url = request.Site.Page // where the impression will be made 148 } 149 150 gdpr := bidGdpr{} 151 152 ccpaPolicy, err := ccpa.ReadFromRequest(request) 153 if err != nil { 154 errs = append(errs, err) 155 } else { 156 body.CCPA = ccpaPolicy.Consent 157 } 158 159 if request.Regs != nil && request.Regs.Ext != nil { 160 var extRegs openrtb_ext.ExtRegs 161 if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil { 162 errs = append(errs, err) 163 } else { 164 if extRegs.GDPR != nil { 165 applies := *extRegs.GDPR != 0 166 gdpr.Applies = &applies 167 body.GDPR = &gdpr 168 } 169 } 170 } 171 172 if request.User != nil && request.User.Ext != nil { 173 var extUser openrtb_ext.ExtUser 174 if err := json.Unmarshal(request.User.Ext, &extUser); err != nil { 175 errs = append(errs, err) 176 } else { 177 gdpr.Consent = extUser.Consent 178 body.GDPR = &gdpr 179 180 if hasEids(extUser.Eids) { 181 body.User.Eids = extUser.Eids 182 } 183 } 184 } 185 186 if request.Source != nil && request.Source.Ext != nil { 187 var extSChain openrtb_ext.ExtRequestPrebidSChain 188 if err := json.Unmarshal(request.Source.Ext, &extSChain); err != nil { 189 errs = append(errs, err) 190 } else { 191 body.SChain = extSChain.SChain 192 } 193 } 194 195 body.Coppa = request.Regs != nil && request.Regs.COPPA > 0 196 197 if request.Regs != nil && request.Regs.GPP != "" { 198 body.GPP = request.Regs.GPP 199 } 200 201 if request.Regs != nil && request.Regs.GPPSID != nil { 202 body.GPPSID = request.Regs.GPPSID 203 } 204 205 if request.Site != nil && request.Site.Content != nil { 206 body.Content = request.Site.Content 207 } else if request.App != nil && request.App.Content != nil { 208 body.Content = request.App.Content 209 } 210 211 for i, impression := range request.Imp { 212 213 _, consumableExt, err := extractExtensions(impression) 214 215 if err != nil { 216 return nil, err 217 } 218 219 // These get set on the first one in observed working requests 220 if i == 0 { 221 body.NetworkId = consumableExt.NetworkId 222 body.SiteId = consumableExt.SiteId 223 body.UnitId = consumableExt.UnitId 224 body.UnitName = consumableExt.UnitName 225 } 226 227 body.Placements[i] = placement{ 228 DivName: impression.ID, 229 NetworkId: consumableExt.NetworkId, 230 SiteId: consumableExt.SiteId, 231 UnitId: consumableExt.UnitId, 232 UnitName: consumableExt.UnitName, 233 AdTypes: getSizeCodes(impression.Banner.Format), // was adTypes: bid.adTypes || getSize(bid.sizes) in prebid.js 234 } 235 } 236 237 bodyBytes, err := json.Marshal(body) 238 if err != nil { 239 return nil, []error{err} 240 } 241 242 requests := []*adapters.RequestData{ 243 { 244 Method: "POST", 245 Uri: "https://e.serverbid.com/api/v2", 246 Body: bodyBytes, 247 Headers: headers, 248 }, 249 } 250 251 return requests, errs 252 } 253 254 /* 255 internal original request in OpenRTB, external = result of us having converted it (what comes out of MakeRequests) 256 */ 257 func (a *ConsumableAdapter) MakeBids( 258 internalRequest *openrtb2.BidRequest, 259 externalRequest *adapters.RequestData, 260 response *adapters.ResponseData, 261 ) (*adapters.BidderResponse, []error) { 262 263 if response.StatusCode == http.StatusNoContent { 264 return nil, nil 265 } 266 267 if response.StatusCode == http.StatusBadRequest { 268 return nil, []error{&errortypes.BadInput{ 269 Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 270 }} 271 } 272 273 if response.StatusCode != http.StatusOK { 274 return nil, []error{&errortypes.BadServerResponse{ 275 Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 276 }} 277 } 278 279 var serverResponse bidResponse // response from Consumable 280 if err := json.Unmarshal(response.Body, &serverResponse); err != nil { 281 return nil, []error{&errortypes.BadServerResponse{ 282 Message: fmt.Sprintf("error while decoding response, err: %s", err), 283 }} 284 } 285 286 bidderResponse := adapters.NewBidderResponse() 287 var errors []error 288 289 for impID, decision := range serverResponse.Decisions { 290 291 if decision.Pricing != nil && decision.Pricing.ClearPrice != nil { 292 bid := openrtb2.Bid{} 293 bid.ID = internalRequest.ID 294 bid.ImpID = impID 295 bid.Price = *decision.Pricing.ClearPrice 296 bid.AdM = retrieveAd(decision) 297 bid.W = int64(decision.Width) 298 bid.H = int64(decision.Height) 299 bid.CrID = strconv.FormatInt(decision.AdID, 10) 300 bid.Exp = 30 // TODO: Check this is intention of TTL 301 bid.ADomain = decision.Adomain 302 bid.Cat = decision.Cats 303 // not yet ported from prebid.js adapter 304 //bid.requestId = bidId; 305 //bid.currency = 'USD'; 306 //bid.netRevenue = true; 307 //bid.referrer = utils.getTopWindowUrl(); 308 309 bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ 310 Bid: &bid, 311 // Consumable units are always HTML, never VAST. 312 // From Prebid's point of view, this means that Consumable units 313 // are always "banners". 314 BidType: openrtb_ext.BidTypeBanner, 315 }) 316 } 317 } 318 return bidderResponse, errors 319 } 320 321 func extractExtensions(impression openrtb2.Imp) (*adapters.ExtImpBidder, *openrtb_ext.ExtImpConsumable, []error) { 322 var bidderExt adapters.ExtImpBidder 323 if err := json.Unmarshal(impression.Ext, &bidderExt); err != nil { 324 return nil, nil, []error{&errortypes.BadInput{ 325 Message: err.Error(), 326 }} 327 } 328 329 var consumableExt openrtb_ext.ExtImpConsumable 330 if err := json.Unmarshal(bidderExt.Bidder, &consumableExt); err != nil { 331 return nil, nil, []error{&errortypes.BadInput{ 332 Message: err.Error(), 333 }} 334 } 335 336 return &bidderExt, &consumableExt, nil 337 } 338 339 // Builder builds a new instance of the Consumable adapter for the given bidder with the given config. 340 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 341 bidder := &ConsumableAdapter{ 342 clock: realInstant{}, 343 endpoint: config.Endpoint, 344 } 345 return bidder, nil 346 } 347 348 func hasEids(eids []openrtb2.EID) bool { 349 for i := 0; i < len(eids); i++ { 350 if len(eids[i].UIDs) > 0 && eids[i].UIDs[0].ID != "" { 351 return true 352 } 353 } 354 return false 355 }