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