github.com/prebid/prebid-server/v2@v2.18.0/adapters/pubmatic/pubmatic.go (about) 1 package pubmatic 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "math" 8 "net/http" 9 "strconv" 10 "strings" 11 12 "github.com/prebid/prebid-server/v2/adapters" 13 "github.com/prebid/prebid-server/v2/config" 14 "github.com/prebid/prebid-server/v2/errortypes" 15 "github.com/prebid/prebid-server/v2/openrtb_ext" 16 "github.com/prebid/prebid-server/v2/util/ptrutil" 17 18 "github.com/buger/jsonparser" 19 "github.com/prebid/openrtb/v20/openrtb2" 20 ) 21 22 const MAX_IMPRESSIONS_PUBMATIC = 30 23 24 const ae = "ae" 25 26 type PubmaticAdapter struct { 27 URI string 28 bidderName string 29 } 30 31 type pubmaticBidExt struct { 32 BidType *int `json:"BidType,omitempty"` 33 VideoCreativeInfo *pubmaticBidExtVideo `json:"video,omitempty"` 34 Marketplace string `json:"marketplace,omitempty"` 35 PrebidDealPriority int `json:"prebiddealpriority,omitempty"` 36 } 37 38 type pubmaticWrapperExt struct { 39 ProfileID int `json:"profile,omitempty"` 40 VersionID int `json:"version,omitempty"` 41 } 42 43 type pubmaticBidExtVideo struct { 44 Duration *int `json:"duration,omitempty"` 45 } 46 47 type ExtImpBidderPubmatic struct { 48 adapters.ExtImpBidder 49 Data json.RawMessage `json:"data,omitempty"` 50 AE int `json:"ae,omitempty"` 51 } 52 53 type ExtAdServer struct { 54 Name string `json:"name"` 55 AdSlot string `json:"adslot"` 56 } 57 58 type marketplaceReqExt struct { 59 AllowedBidders []string `json:"allowedbidders,omitempty"` 60 } 61 62 type extRequestAdServer struct { 63 Wrapper *pubmaticWrapperExt `json:"wrapper,omitempty"` 64 Acat []string `json:"acat,omitempty"` 65 Marketplace *marketplaceReqExt `json:"marketplace,omitempty"` 66 openrtb_ext.ExtRequest 67 } 68 69 type respExt struct { 70 FledgeAuctionConfigs map[string]json.RawMessage `json:"fledge_auction_configs,omitempty"` 71 } 72 73 const ( 74 dctrKeyName = "key_val" 75 pmZoneIDKeyName = "pmZoneId" 76 pmZoneIDKeyNameOld = "pmZoneID" 77 ImpExtAdUnitKey = "dfp_ad_unit_code" 78 AdServerGAM = "gam" 79 AdServerKey = "adserver" 80 PBAdslotKey = "pbadslot" 81 ) 82 83 func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 84 errs := make([]error, 0, len(request.Imp)) 85 86 pubID := "" 87 extractWrapperExtFromImp := true 88 extractPubIDFromImp := true 89 90 newReqExt, err := extractPubmaticExtFromRequest(request) 91 if err != nil { 92 return nil, []error{err} 93 } 94 wrapperExt := newReqExt.Wrapper 95 if wrapperExt != nil && wrapperExt.ProfileID != 0 && wrapperExt.VersionID != 0 { 96 extractWrapperExtFromImp = false 97 } 98 99 for i := 0; i < len(request.Imp); i++ { 100 wrapperExtFromImp, pubIDFromImp, err := parseImpressionObject(&request.Imp[i], extractWrapperExtFromImp, extractPubIDFromImp) 101 102 // If the parsing is failed, remove imp and add the error. 103 if err != nil { 104 errs = append(errs, err) 105 request.Imp = append(request.Imp[:i], request.Imp[i+1:]...) 106 i-- 107 continue 108 } 109 110 if extractWrapperExtFromImp { 111 if wrapperExtFromImp != nil { 112 if wrapperExt == nil { 113 wrapperExt = &pubmaticWrapperExt{} 114 } 115 if wrapperExt.ProfileID == 0 { 116 wrapperExt.ProfileID = wrapperExtFromImp.ProfileID 117 } 118 if wrapperExt.VersionID == 0 { 119 wrapperExt.VersionID = wrapperExtFromImp.VersionID 120 } 121 122 if wrapperExt != nil && wrapperExt.ProfileID != 0 && wrapperExt.VersionID != 0 { 123 extractWrapperExtFromImp = false 124 } 125 } 126 } 127 128 if extractPubIDFromImp && pubIDFromImp != "" { 129 pubID = pubIDFromImp 130 extractPubIDFromImp = false 131 } 132 } 133 134 // If all the requests are invalid, Call to adaptor is skipped 135 if len(request.Imp) == 0 { 136 return nil, errs 137 } 138 139 newReqExt.Wrapper = wrapperExt 140 rawExt, err := json.Marshal(newReqExt) 141 if err != nil { 142 return nil, []error{err} 143 } 144 request.Ext = rawExt 145 146 if request.Site != nil { 147 siteCopy := *request.Site 148 if siteCopy.Publisher != nil { 149 publisherCopy := *siteCopy.Publisher 150 publisherCopy.ID = pubID 151 siteCopy.Publisher = &publisherCopy 152 } else { 153 siteCopy.Publisher = &openrtb2.Publisher{ID: pubID} 154 } 155 request.Site = &siteCopy 156 } else if request.App != nil { 157 appCopy := *request.App 158 if appCopy.Publisher != nil { 159 publisherCopy := *appCopy.Publisher 160 publisherCopy.ID = pubID 161 appCopy.Publisher = &publisherCopy 162 } else { 163 appCopy.Publisher = &openrtb2.Publisher{ID: pubID} 164 } 165 request.App = &appCopy 166 } 167 168 reqJSON, err := json.Marshal(request) 169 if err != nil { 170 errs = append(errs, err) 171 return nil, errs 172 } 173 174 headers := http.Header{} 175 headers.Add("Content-Type", "application/json;charset=utf-8") 176 headers.Add("Accept", "application/json") 177 return []*adapters.RequestData{{ 178 Method: "POST", 179 Uri: a.URI, 180 Body: reqJSON, 181 Headers: headers, 182 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 183 }}, errs 184 } 185 186 // validateAdslot validate the optional adslot string 187 // valid formats are 'adslot@WxH', 'adslot' and no adslot 188 func validateAdSlot(adslot string, imp *openrtb2.Imp) error { 189 adSlotStr := strings.TrimSpace(adslot) 190 191 if len(adSlotStr) == 0 { 192 return nil 193 } 194 195 if !strings.Contains(adSlotStr, "@") { 196 imp.TagID = adSlotStr 197 return nil 198 } 199 200 adSlot := strings.Split(adSlotStr, "@") 201 if len(adSlot) == 2 && adSlot[0] != "" && adSlot[1] != "" { 202 imp.TagID = strings.TrimSpace(adSlot[0]) 203 204 adSize := strings.Split(strings.ToLower(adSlot[1]), "x") 205 if len(adSize) != 2 { 206 return fmt.Errorf("Invalid size provided in adSlot %v", adSlotStr) 207 } 208 209 width, err := strconv.Atoi(strings.TrimSpace(adSize[0])) 210 if err != nil { 211 return fmt.Errorf("Invalid width provided in adSlot %v", adSlotStr) 212 } 213 214 heightStr := strings.Split(adSize[1], ":") 215 height, err := strconv.Atoi(strings.TrimSpace(heightStr[0])) 216 if err != nil { 217 return fmt.Errorf("Invalid height provided in adSlot %v", adSlotStr) 218 } 219 220 //In case of video, size could be derived from the player size 221 if imp.Banner != nil { 222 imp.Banner = assignBannerWidthAndHeight(imp.Banner, int64(width), int64(height)) 223 } 224 } else { 225 return fmt.Errorf("Invalid adSlot %v", adSlotStr) 226 } 227 228 return nil 229 } 230 231 func assignBannerSize(banner *openrtb2.Banner) (*openrtb2.Banner, error) { 232 if banner.W != nil && banner.H != nil { 233 return banner, nil 234 } 235 236 return assignBannerWidthAndHeight(banner, banner.Format[0].W, banner.Format[0].H), nil 237 } 238 239 func assignBannerWidthAndHeight(banner *openrtb2.Banner, w, h int64) *openrtb2.Banner { 240 bannerCopy := *banner 241 bannerCopy.W = ptrutil.ToPtr(w) 242 bannerCopy.H = ptrutil.ToPtr(h) 243 return &bannerCopy 244 } 245 246 // parseImpressionObject parse the imp to get it ready to send to pubmatic 247 func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractPubIDFromImp bool) (*pubmaticWrapperExt, string, error) { 248 var wrapExt *pubmaticWrapperExt 249 var pubID string 250 251 // PubMatic supports banner and video impressions. 252 if imp.Banner == nil && imp.Video == nil && imp.Native == nil { 253 return wrapExt, pubID, fmt.Errorf("invalid MediaType. PubMatic only supports Banner, Video and Native. Ignoring ImpID=%s", imp.ID) 254 } 255 256 if imp.Audio != nil { 257 imp.Audio = nil 258 } 259 260 var bidderExt ExtImpBidderPubmatic 261 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 262 return wrapExt, pubID, err 263 } 264 265 var pubmaticExt openrtb_ext.ExtImpPubmatic 266 if err := json.Unmarshal(bidderExt.Bidder, &pubmaticExt); err != nil { 267 return wrapExt, pubID, err 268 } 269 270 if extractPubIDFromImp { 271 pubID = strings.TrimSpace(pubmaticExt.PublisherId) 272 } 273 274 // Parse Wrapper Extension only once per request 275 if extractWrapperExtFromImp && len(pubmaticExt.WrapExt) != 0 { 276 err := json.Unmarshal([]byte(pubmaticExt.WrapExt), &wrapExt) 277 if err != nil { 278 return wrapExt, pubID, fmt.Errorf("Error in Wrapper Parameters = %v for ImpID = %v WrapperExt = %v", err.Error(), imp.ID, string(pubmaticExt.WrapExt)) 279 } 280 } 281 282 if err := validateAdSlot(strings.TrimSpace(pubmaticExt.AdSlot), imp); err != nil { 283 return wrapExt, pubID, err 284 } 285 286 if imp.Banner != nil { 287 bannerCopy, err := assignBannerSize(imp.Banner) 288 if err != nil { 289 return wrapExt, pubID, err 290 } 291 imp.Banner = bannerCopy 292 } 293 294 if pubmaticExt.Kadfloor != "" { 295 bidfloor, err := strconv.ParseFloat(strings.TrimSpace(pubmaticExt.Kadfloor), 64) 296 if err == nil { 297 // In case of valid kadfloor, select maximum of original imp.bidfloor and kadfloor 298 imp.BidFloor = math.Max(bidfloor, imp.BidFloor) 299 } 300 } 301 302 extMap := make(map[string]interface{}, 0) 303 if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 { 304 addKeywordsToExt(pubmaticExt.Keywords, extMap) 305 } 306 //Give preference to direct values of 'dctr' & 'pmZoneId' params in extension 307 if pubmaticExt.Dctr != "" { 308 extMap[dctrKeyName] = pubmaticExt.Dctr 309 } 310 if pubmaticExt.PmZoneID != "" { 311 extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID 312 } 313 314 if len(bidderExt.Data) > 0 { 315 populateFirstPartyDataImpAttributes(bidderExt.Data, extMap) 316 } 317 318 if bidderExt.AE != 0 { 319 extMap[ae] = bidderExt.AE 320 } 321 322 imp.Ext = nil 323 if len(extMap) > 0 { 324 ext, err := json.Marshal(extMap) 325 if err == nil { 326 imp.Ext = ext 327 } 328 } 329 330 return wrapExt, pubID, nil 331 } 332 333 // extractPubmaticExtFromRequest parse the req.ext to fetch wrapper and acat params 334 func extractPubmaticExtFromRequest(request *openrtb2.BidRequest) (extRequestAdServer, error) { 335 // req.ext.prebid would always be there and Less nil cases to handle, more safe! 336 var pmReqExt extRequestAdServer 337 338 if request == nil || len(request.Ext) == 0 { 339 return pmReqExt, nil 340 } 341 342 reqExt := &openrtb_ext.ExtRequest{} 343 err := json.Unmarshal(request.Ext, &reqExt) 344 if err != nil { 345 return pmReqExt, fmt.Errorf("error decoding Request.ext : %s", err.Error()) 346 } 347 pmReqExt.ExtRequest = *reqExt 348 349 reqExtBidderParams := make(map[string]json.RawMessage) 350 if reqExt.Prebid.BidderParams != nil { 351 err = json.Unmarshal(reqExt.Prebid.BidderParams, &reqExtBidderParams) 352 if err != nil { 353 return pmReqExt, err 354 } 355 } 356 357 //get request ext bidder params 358 if wrapperObj, present := reqExtBidderParams["wrapper"]; present && len(wrapperObj) != 0 { 359 wrpExt := &pubmaticWrapperExt{} 360 err = json.Unmarshal(wrapperObj, wrpExt) 361 if err != nil { 362 return pmReqExt, err 363 } 364 pmReqExt.Wrapper = wrpExt 365 } 366 367 if acatBytes, ok := reqExtBidderParams["acat"]; ok { 368 var acat []string 369 err = json.Unmarshal(acatBytes, &acat) 370 if err != nil { 371 return pmReqExt, err 372 } 373 for i := 0; i < len(acat); i++ { 374 acat[i] = strings.TrimSpace(acat[i]) 375 } 376 pmReqExt.Acat = acat 377 } 378 379 if allowedBidders := getAlternateBidderCodesFromRequestExt(reqExt); allowedBidders != nil { 380 pmReqExt.Marketplace = &marketplaceReqExt{AllowedBidders: allowedBidders} 381 } 382 383 return pmReqExt, nil 384 } 385 386 func getAlternateBidderCodesFromRequestExt(reqExt *openrtb_ext.ExtRequest) []string { 387 if reqExt == nil || reqExt.Prebid.AlternateBidderCodes == nil { 388 return nil 389 } 390 391 allowedBidders := []string{"pubmatic"} 392 if reqExt.Prebid.AlternateBidderCodes.Enabled { 393 if pmABC, ok := reqExt.Prebid.AlternateBidderCodes.Bidders["pubmatic"]; ok && pmABC.Enabled { 394 if pmABC.AllowedBidderCodes == nil || (len(pmABC.AllowedBidderCodes) == 1 && pmABC.AllowedBidderCodes[0] == "*") { 395 return []string{"all"} 396 } 397 return append(allowedBidders, pmABC.AllowedBidderCodes...) 398 } 399 } 400 401 return allowedBidders 402 } 403 404 func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) { 405 for _, keyVal := range keywords { 406 if len(keyVal.Values) == 0 { 407 continue 408 } else { 409 key := keyVal.Key 410 if keyVal.Key == pmZoneIDKeyNameOld { 411 key = pmZoneIDKeyName 412 } 413 extMap[key] = strings.Join(keyVal.Values[:], ",") 414 } 415 } 416 } 417 418 func (a *PubmaticAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 419 if response.StatusCode == http.StatusNoContent { 420 return nil, nil 421 } 422 423 if response.StatusCode == http.StatusBadRequest { 424 return nil, []error{&errortypes.BadInput{ 425 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 426 }} 427 } 428 429 if response.StatusCode != http.StatusOK { 430 return nil, []error{fmt.Errorf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode)} 431 } 432 433 var bidResp openrtb2.BidResponse 434 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 435 return nil, []error{err} 436 } 437 438 bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) 439 440 var errs []error 441 for _, sb := range bidResp.SeatBid { 442 for i := 0; i < len(sb.Bid); i++ { 443 bid := sb.Bid[i] 444 if len(bid.Cat) > 1 { 445 bid.Cat = bid.Cat[0:1] 446 } 447 448 typedBid := &adapters.TypedBid{ 449 Bid: &bid, 450 BidType: openrtb_ext.BidTypeBanner, 451 BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, 452 } 453 454 var bidExt *pubmaticBidExt 455 err := json.Unmarshal(bid.Ext, &bidExt) 456 if err != nil { 457 errs = append(errs, err) 458 } else if bidExt != nil { 459 typedBid.Seat = openrtb_ext.BidderName(bidExt.Marketplace) 460 typedBid.BidType = getBidType(bidExt) 461 if bidExt.PrebidDealPriority > 0 { 462 typedBid.DealPriority = bidExt.PrebidDealPriority 463 } 464 465 if bidExt.VideoCreativeInfo != nil && bidExt.VideoCreativeInfo.Duration != nil { 466 typedBid.BidVideo.Duration = *bidExt.VideoCreativeInfo.Duration 467 } 468 } 469 470 if typedBid.BidType == openrtb_ext.BidTypeNative { 471 bid.AdM, err = getNativeAdm(bid.AdM) 472 if err != nil { 473 errs = append(errs, err) 474 } 475 } 476 477 bidResponse.Bids = append(bidResponse.Bids, typedBid) 478 } 479 } 480 if bidResp.Cur != "" { 481 bidResponse.Currency = bidResp.Cur 482 } 483 484 if bidResp.Ext != nil { 485 var bidRespExt respExt 486 if err := json.Unmarshal(bidResp.Ext, &bidRespExt); err == nil && bidRespExt.FledgeAuctionConfigs != nil { 487 bidResponse.FledgeAuctionConfigs = make([]*openrtb_ext.FledgeAuctionConfig, 0, len(bidRespExt.FledgeAuctionConfigs)) 488 for impId, config := range bidRespExt.FledgeAuctionConfigs { 489 fledgeAuctionConfig := &openrtb_ext.FledgeAuctionConfig{ 490 ImpId: impId, 491 Config: config, 492 } 493 bidResponse.FledgeAuctionConfigs = append(bidResponse.FledgeAuctionConfigs, fledgeAuctionConfig) 494 } 495 } 496 } 497 return bidResponse, errs 498 } 499 500 func getNativeAdm(adm string) (string, error) { 501 var err error 502 nativeAdm := make(map[string]interface{}) 503 err = json.Unmarshal([]byte(adm), &nativeAdm) 504 if err != nil { 505 return adm, errors.New("unable to unmarshal native adm") 506 } 507 508 // move bid.adm.native to bid.adm 509 if _, ok := nativeAdm["native"]; ok { 510 //using jsonparser to avoid marshaling, encode escape, etc. 511 value, _, _, err := jsonparser.Get([]byte(adm), string(openrtb_ext.BidTypeNative)) 512 if err != nil { 513 return adm, errors.New("unable to get native adm") 514 } 515 adm = string(value) 516 } 517 518 return adm, nil 519 } 520 521 // getMapFromJSON converts JSON to map 522 func getMapFromJSON(source json.RawMessage) map[string]interface{} { 523 if source != nil { 524 dataMap := make(map[string]interface{}) 525 err := json.Unmarshal(source, &dataMap) 526 if err == nil { 527 return dataMap 528 } 529 } 530 return nil 531 } 532 533 // populateFirstPartyDataImpAttributes will parse imp.ext.data and populate imp extMap 534 func populateFirstPartyDataImpAttributes(data json.RawMessage, extMap map[string]interface{}) { 535 536 dataMap := getMapFromJSON(data) 537 538 if dataMap == nil { 539 return 540 } 541 542 populateAdUnitKey(data, dataMap, extMap) 543 populateDctrKey(dataMap, extMap) 544 } 545 546 // populateAdUnitKey parses data object to read and populate DFP adunit key 547 func populateAdUnitKey(data json.RawMessage, dataMap, extMap map[string]interface{}) { 548 549 if name, err := jsonparser.GetString(data, "adserver", "name"); err == nil && name == AdServerGAM { 550 if adslot, err := jsonparser.GetString(data, "adserver", "adslot"); err == nil && adslot != "" { 551 extMap[ImpExtAdUnitKey] = adslot 552 } 553 } 554 555 //imp.ext.dfp_ad_unit_code is not set, then check pbadslot in imp.ext.data 556 if extMap[ImpExtAdUnitKey] == nil && dataMap[PBAdslotKey] != nil { 557 extMap[ImpExtAdUnitKey] = dataMap[PBAdslotKey].(string) 558 } 559 } 560 561 // populateDctrKey reads key-val pairs from imp.ext.data and add it in imp.ext.key_val 562 func populateDctrKey(dataMap, extMap map[string]interface{}) { 563 var dctr strings.Builder 564 565 //append dctr key if already present in extMap 566 if extMap[dctrKeyName] != nil { 567 dctr.WriteString(extMap[dctrKeyName].(string)) 568 } 569 570 for key, val := range dataMap { 571 572 //ignore 'pbaslot' and 'adserver' key as they are not targeting keys 573 if key == PBAdslotKey || key == AdServerKey { 574 continue 575 } 576 577 //separate key-val pairs in dctr string by pipe(|) 578 if dctr.Len() > 0 { 579 dctr.WriteString("|") 580 } 581 582 //trimming spaces from key 583 key = strings.TrimSpace(key) 584 585 switch typedValue := val.(type) { 586 case string: 587 if _, err := fmt.Fprintf(&dctr, "%s=%s", key, strings.TrimSpace(typedValue)); err != nil { 588 continue 589 } 590 591 case float64, bool: 592 if _, err := fmt.Fprintf(&dctr, "%s=%v", key, typedValue); err != nil { 593 continue 594 } 595 596 case []interface{}: 597 if valStrArr := getStringArray(typedValue); len(valStrArr) > 0 { 598 valStr := strings.Join(valStrArr[:], ",") 599 if _, err := fmt.Fprintf(&dctr, "%s=%s", key, valStr); err != nil { 600 continue 601 } 602 } 603 } 604 } 605 606 if dctrStr := dctr.String(); dctrStr != "" { 607 extMap[dctrKeyName] = strings.TrimSuffix(dctrStr, "|") 608 } 609 } 610 611 // getStringArray converts interface of type string array to string array 612 func getStringArray(array []interface{}) []string { 613 aString := make([]string, len(array)) 614 for i, v := range array { 615 if str, ok := v.(string); ok { 616 aString[i] = strings.TrimSpace(str) 617 } else { 618 return nil 619 } 620 } 621 return aString 622 } 623 624 // getBidType returns the bid type specified in the response bid.ext 625 func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType { 626 // setting "banner" as the default bid type 627 bidType := openrtb_ext.BidTypeBanner 628 if bidExt != nil && bidExt.BidType != nil { 629 switch *bidExt.BidType { 630 case 0: 631 bidType = openrtb_ext.BidTypeBanner 632 case 1: 633 bidType = openrtb_ext.BidTypeVideo 634 case 2: 635 bidType = openrtb_ext.BidTypeNative 636 default: 637 // default value is banner 638 bidType = openrtb_ext.BidTypeBanner 639 } 640 } 641 return bidType 642 } 643 644 // Builder builds a new instance of the Pubmatic adapter for the given bidder with the given config. 645 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 646 bidder := &PubmaticAdapter{ 647 URI: config.Endpoint, 648 bidderName: string(bidderName), 649 } 650 return bidder, nil 651 }