github.com/prebid/prebid-server/v2@v2.18.0/adapters/adnuntius/adnuntius.go (about) 1 package adnuntius 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strconv" 9 "strings" 10 11 "github.com/buger/jsonparser" 12 "github.com/prebid/openrtb/v20/openrtb2" 13 "github.com/prebid/prebid-server/v2/adapters" 14 "github.com/prebid/prebid-server/v2/config" 15 "github.com/prebid/prebid-server/v2/errortypes" 16 "github.com/prebid/prebid-server/v2/openrtb_ext" 17 "github.com/prebid/prebid-server/v2/util/timeutil" 18 ) 19 20 type QueryString map[string]string 21 type adapter struct { 22 time timeutil.Time 23 endpoint string 24 extraInfo string 25 } 26 type adnAdunit struct { 27 AuId string `json:"auId"` 28 TargetId string `json:"targetId"` 29 Dimensions [][]int64 `json:"dimensions,omitempty"` 30 MaxDeals int `json:"maxDeals,omitempty"` 31 } 32 33 type extDeviceAdnuntius struct { 34 NoCookies bool `json:"noCookies,omitempty"` 35 } 36 type siteExt struct { 37 Data interface{} `json:"data"` 38 } 39 40 type Ad struct { 41 Bid struct { 42 Amount float64 43 Currency string 44 } 45 NetBid struct { 46 Amount float64 47 } 48 GrossBid struct { 49 Amount float64 50 } 51 DealID string `json:"dealId,omitempty"` 52 AdId string 53 CreativeWidth string 54 CreativeHeight string 55 CreativeId string 56 LineItemId string 57 Html string 58 DestinationUrls map[string]string 59 } 60 61 type AdUnit struct { 62 AuId string 63 TargetId string 64 Html string 65 ResponseId string 66 Ads []Ad 67 Deals []Ad `json:"deals,omitempty"` 68 } 69 70 type AdnResponse struct { 71 AdUnits []AdUnit 72 } 73 type adnMetaData struct { 74 Usi string `json:"usi,omitempty"` 75 } 76 type adnRequest struct { 77 AdUnits []adnAdunit `json:"adUnits"` 78 MetaData adnMetaData `json:"metaData,omitempty"` 79 Context string `json:"context,omitempty"` 80 KeyValues interface{} `json:"kv,omitempty"` 81 } 82 83 type RequestExt struct { 84 Bidder adnAdunit `json:"bidder"` 85 } 86 87 const defaultNetwork = "default" 88 const defaultSite = "unknown" 89 const minutesInHour = 60 90 91 // Builder builds a new instance of the Adnuntius adapter for the given bidder with the given config. 92 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 93 bidder := &adapter{ 94 time: &timeutil.RealTime{}, 95 endpoint: config.Endpoint, 96 extraInfo: config.ExtraAdapterInfo, 97 } 98 99 return bidder, nil 100 } 101 102 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 103 return a.generateRequests(*request) 104 } 105 106 func setHeaders(ortbRequest openrtb2.BidRequest) http.Header { 107 108 headers := http.Header{} 109 headers.Add("Content-Type", "application/json;charset=utf-8") 110 headers.Add("Accept", "application/json") 111 if ortbRequest.Device != nil { 112 if ortbRequest.Device.IP != "" { 113 headers.Add("X-Forwarded-For", ortbRequest.Device.IP) 114 } 115 if ortbRequest.Device.UA != "" { 116 headers.Add("user-agent", ortbRequest.Device.UA) 117 } 118 } 119 return headers 120 } 121 122 func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter, noCookies bool) (string, []error) { 123 uri, err := url.Parse(a.endpoint) 124 endpointUrl := a.endpoint 125 if err != nil { 126 return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)} 127 } 128 129 gdpr, consent, err := getGDPR(&ortbRequest) 130 if err != nil { 131 return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)} 132 } 133 134 if !noCookies { 135 var deviceExt extDeviceAdnuntius 136 if ortbRequest.Device != nil && ortbRequest.Device.Ext != nil { 137 if err := json.Unmarshal(ortbRequest.Device.Ext, &deviceExt); err != nil { 138 return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)} 139 } 140 } 141 142 if deviceExt.NoCookies { 143 noCookies = true 144 } 145 } 146 147 _, offset := a.time.Now().Zone() 148 tzo := -offset / minutesInHour 149 150 q := uri.Query() 151 if gdpr != "" { 152 endpointUrl = a.extraInfo 153 q.Set("gdpr", gdpr) 154 } 155 156 if consent != "" { 157 q.Set("consentString", consent) 158 } 159 160 if noCookies { 161 q.Set("noCookies", "true") 162 } 163 164 q.Set("tzo", fmt.Sprint(tzo)) 165 q.Set("format", "json") 166 167 url := endpointUrl + "?" + q.Encode() 168 return url, nil 169 } 170 171 func getImpSizes(imp openrtb2.Imp) [][]int64 { 172 173 if len(imp.Banner.Format) > 0 { 174 sizes := make([][]int64, len(imp.Banner.Format)) 175 for i, format := range imp.Banner.Format { 176 sizes[i] = []int64{format.W, format.H} 177 } 178 179 return sizes 180 } 181 182 if imp.Banner.W != nil && imp.Banner.H != nil { 183 size := make([][]int64, 1) 184 size[0] = []int64{*imp.Banner.W, *imp.Banner.H} 185 return size 186 } 187 188 return nil 189 } 190 191 /* 192 Generate the requests to Adnuntius to reduce the amount of requests going out. 193 */ 194 func (a *adapter) generateRequests(ortbRequest openrtb2.BidRequest) ([]*adapters.RequestData, []error) { 195 var requestData []*adapters.RequestData 196 networkAdunitMap := make(map[string][]adnAdunit) 197 headers := setHeaders(ortbRequest) 198 var noCookies bool = false 199 200 for _, imp := range ortbRequest.Imp { 201 if imp.Banner == nil { 202 return nil, []error{&errortypes.BadInput{ 203 Message: fmt.Sprintf("ignoring imp id=%s, Adnuntius supports only Banner", imp.ID), 204 }} 205 } 206 207 var bidderExt adapters.ExtImpBidder 208 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 209 return nil, []error{&errortypes.BadInput{ 210 Message: fmt.Sprintf("Error unmarshalling ExtImpBidder: %s", err.Error()), 211 }} 212 } 213 214 var adnuntiusExt openrtb_ext.ImpExtAdnunitus 215 if err := json.Unmarshal(bidderExt.Bidder, &adnuntiusExt); err != nil { 216 return nil, []error{&errortypes.BadInput{ 217 Message: fmt.Sprintf("Error unmarshalling ExtImpValues: %s", err.Error()), 218 }} 219 } 220 221 if adnuntiusExt.NoCookies { 222 noCookies = true 223 } 224 225 network := defaultNetwork 226 if adnuntiusExt.Network != "" { 227 network = adnuntiusExt.Network 228 } 229 230 adUnit := adnAdunit{ 231 AuId: adnuntiusExt.Auid, 232 TargetId: fmt.Sprintf("%s-%s", adnuntiusExt.Auid, imp.ID), 233 Dimensions: getImpSizes(imp), 234 } 235 if adnuntiusExt.MaxDeals > 0 { 236 adUnit.MaxDeals = adnuntiusExt.MaxDeals 237 } 238 networkAdunitMap[network] = append( 239 networkAdunitMap[network], 240 adUnit) 241 } 242 243 endpoint, err := makeEndpointUrl(ortbRequest, a, noCookies) 244 if err != nil { 245 return nil, []error{&errortypes.BadInput{ 246 Message: fmt.Sprintf("failed to parse URL: %s", err), 247 }} 248 } 249 250 site := defaultSite 251 if ortbRequest.Site != nil && ortbRequest.Site.Page != "" { 252 site = ortbRequest.Site.Page 253 } 254 255 extSite, erro := getSiteExtAsKv(&ortbRequest) 256 if erro != nil { 257 return nil, []error{fmt.Errorf("failed to parse site Ext: %v", err)} 258 } 259 260 for _, networkAdunits := range networkAdunitMap { 261 262 adnuntiusRequest := adnRequest{ 263 AdUnits: networkAdunits, 264 Context: site, 265 KeyValues: extSite.Data, 266 } 267 268 var extUser openrtb_ext.ExtUser 269 if ortbRequest.User != nil && ortbRequest.User.Ext != nil { 270 if err := json.Unmarshal(ortbRequest.User.Ext, &extUser); err != nil { 271 return nil, []error{fmt.Errorf("failed to parse Ext User: %v", err)} 272 } 273 } 274 275 // Will change when our adserver can accept multiple user IDS 276 if extUser.Eids != nil && len(extUser.Eids) > 0 { 277 if len(extUser.Eids[0].UIDs) > 0 { 278 adnuntiusRequest.MetaData.Usi = extUser.Eids[0].UIDs[0].ID 279 } 280 } 281 282 ortbUser := ortbRequest.User 283 if ortbUser != nil { 284 ortbUserId := ortbRequest.User.ID 285 if ortbUserId != "" { 286 adnuntiusRequest.MetaData.Usi = ortbRequest.User.ID 287 } 288 } 289 290 adnJson, err := json.Marshal(adnuntiusRequest) 291 if err != nil { 292 return nil, []error{&errortypes.BadInput{ 293 Message: fmt.Sprintf("Error unmarshalling adnuntius request: %s", err.Error()), 294 }} 295 } 296 297 requestData = append(requestData, &adapters.RequestData{ 298 Method: http.MethodPost, 299 Uri: endpoint, 300 Body: adnJson, 301 Headers: headers, 302 ImpIDs: openrtb_ext.GetImpIDs(ortbRequest.Imp), 303 }) 304 305 } 306 307 return requestData, nil 308 } 309 310 func (a *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 311 312 if response.StatusCode == http.StatusBadRequest { 313 return nil, []error{&errortypes.BadInput{ 314 Message: fmt.Sprintf("Status code: %d, Request malformed", response.StatusCode), 315 }} 316 } 317 318 if response.StatusCode != http.StatusOK { 319 return nil, []error{&errortypes.BadServerResponse{ 320 Message: fmt.Sprintf("Status code: %d, Something went wrong with your request", response.StatusCode), 321 }} 322 } 323 324 var adnResponse AdnResponse 325 if err := json.Unmarshal(response.Body, &adnResponse); err != nil { 326 return nil, []error{err} 327 } 328 329 bidResponse, bidErr := generateBidResponse(&adnResponse, request) 330 if bidErr != nil { 331 return nil, bidErr 332 } 333 334 return bidResponse, nil 335 } 336 337 func getSiteExtAsKv(request *openrtb2.BidRequest) (siteExt, error) { 338 var extSite siteExt 339 if request.Site != nil && request.Site.Ext != nil { 340 if err := json.Unmarshal(request.Site.Ext, &extSite); err != nil { 341 return extSite, fmt.Errorf("failed to parse ExtSite in Adnuntius: %v", err) 342 } 343 } 344 return extSite, nil 345 } 346 347 func getGDPR(request *openrtb2.BidRequest) (string, string, error) { 348 349 gdpr := "" 350 var extRegs openrtb_ext.ExtRegs 351 if request.Regs != nil && request.Regs.Ext != nil { 352 if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil { 353 return "", "", fmt.Errorf("failed to parse ExtRegs in Adnuntius GDPR check: %v", err) 354 } 355 if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) { 356 gdpr = strconv.Itoa(int(*extRegs.GDPR)) 357 } 358 } 359 360 consent := "" 361 if request.User != nil && request.User.Ext != nil { 362 var extUser openrtb_ext.ExtUser 363 if err := json.Unmarshal(request.User.Ext, &extUser); err != nil { 364 return "", "", fmt.Errorf("failed to parse ExtUser in Adnuntius GDPR check: %v", err) 365 } 366 consent = extUser.Consent 367 } 368 369 return gdpr, consent, nil 370 } 371 372 func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2.BidRequest) (*openrtb2.Bid, []error) { 373 374 creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64) 375 if widthErr != nil { 376 return nil, []error{&errortypes.BadInput{ 377 Message: fmt.Sprintf("Value of width: %s is not a string", ad.CreativeWidth), 378 }} 379 } 380 381 creativeHeight, heightErr := strconv.ParseInt(ad.CreativeHeight, 10, 64) 382 if heightErr != nil { 383 return nil, []error{&errortypes.BadInput{ 384 Message: fmt.Sprintf("Value of height: %s is not a string", ad.CreativeHeight), 385 }} 386 } 387 388 price := ad.Bid.Amount 389 390 var bidderExt adapters.ExtImpBidder 391 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 392 return nil, []error{&errortypes.BadInput{ 393 Message: fmt.Sprintf("Error unmarshalling ExtImpBidder: %s", err.Error()), 394 }} 395 } 396 397 var adnuntiusExt openrtb_ext.ImpExtAdnunitus 398 if err := json.Unmarshal(bidderExt.Bidder, &adnuntiusExt); err != nil { 399 return nil, []error{&errortypes.BadInput{ 400 Message: fmt.Sprintf("Error unmarshalling ExtImpValues: %s", err.Error()), 401 }} 402 } 403 404 if adnuntiusExt.BidType != "" { 405 if strings.EqualFold(string(adnuntiusExt.BidType), "net") { 406 price = ad.NetBid.Amount 407 } 408 if strings.EqualFold(string(adnuntiusExt.BidType), "gross") { 409 price = ad.GrossBid.Amount 410 } 411 } 412 413 adDomain := []string{} 414 for _, url := range ad.DestinationUrls { 415 domainArray := strings.Split(url, "/") 416 domain := strings.Replace(domainArray[2], "www.", "", -1) 417 adDomain = append(adDomain, domain) 418 } 419 420 bid := openrtb2.Bid{ 421 ID: ad.AdId, 422 ImpID: imp.ID, 423 W: creativeWidth, 424 H: creativeHeight, 425 AdID: ad.AdId, 426 DealID: ad.DealID, 427 CID: ad.LineItemId, 428 CrID: ad.CreativeId, 429 Price: price * 1000, 430 AdM: html, 431 ADomain: adDomain, 432 } 433 return &bid, nil 434 435 } 436 437 func generateBidResponse(adnResponse *AdnResponse, request *openrtb2.BidRequest) (*adapters.BidderResponse, []error) { 438 bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(adnResponse.AdUnits)) 439 var currency string 440 adunitMap := map[string]AdUnit{} 441 442 for _, adnRespAdunit := range adnResponse.AdUnits { 443 adunitMap[adnRespAdunit.TargetId] = adnRespAdunit 444 } 445 446 for _, imp := range request.Imp { 447 448 auId, _, _, err := jsonparser.Get(imp.Ext, "bidder", "auId") 449 if err != nil { 450 return nil, []error{&errortypes.BadInput{ 451 Message: fmt.Sprintf("Error at Bidder auId: %s", err.Error()), 452 }} 453 } 454 455 targetID := fmt.Sprintf("%s-%s", string(auId), imp.ID) 456 adunit := adunitMap[targetID] 457 458 if len(adunit.Ads) > 0 { 459 460 ad := adunit.Ads[0] 461 currency = ad.Bid.Currency 462 463 adBid, err := generateAdResponse(ad, imp, adunit.Html, request) 464 if err != nil { 465 return nil, []error{&errortypes.BadInput{ 466 Message: "Error at ad generation", 467 }} 468 } 469 470 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 471 Bid: adBid, 472 BidType: "banner", 473 }) 474 475 for _, deal := range adunit.Deals { 476 dealBid, err := generateAdResponse(deal, imp, deal.Html, request) 477 if err != nil { 478 return nil, []error{&errortypes.BadInput{ 479 Message: "Error at ad generation", 480 }} 481 } 482 483 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 484 Bid: dealBid, 485 BidType: "banner", 486 }) 487 } 488 489 } 490 491 } 492 bidResponse.Currency = currency 493 return bidResponse, nil 494 }