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