github.com/prebid/prebid-server/v2@v2.18.0/adapters/smaato/smaato.go (about) 1 package smaato 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "strconv" 8 "strings" 9 10 "github.com/buger/jsonparser" 11 "github.com/prebid/openrtb/v20/openrtb2" 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/metrics" 16 "github.com/prebid/prebid-server/v2/openrtb_ext" 17 "github.com/prebid/prebid-server/v2/util/ptrutil" 18 "github.com/prebid/prebid-server/v2/util/timeutil" 19 ) 20 21 const clientVersion = "prebid_server_0.6" 22 23 type adMarkupType string 24 25 const ( 26 smtAdTypeImg adMarkupType = "Img" 27 smtAdTypeRichmedia adMarkupType = "Richmedia" 28 smtAdTypeVideo adMarkupType = "Video" 29 smtAdTypeNative adMarkupType = "Native" 30 ) 31 32 // adapter describes a Smaato prebid server adapter. 33 type adapter struct { 34 clock timeutil.Time 35 endpoint string 36 } 37 38 // userExtData defines User.Ext.Data object for Smaato 39 type userExtData struct { 40 Keywords string `json:"keywords"` 41 Gender string `json:"gender"` 42 Yob int64 `json:"yob"` 43 } 44 45 // siteExt defines Site.Ext object for Smaato 46 type siteExt struct { 47 Data siteExtData `json:"data"` 48 } 49 50 type siteExtData struct { 51 Keywords string `json:"keywords"` 52 } 53 54 // bidRequestExt defines BidRequest.Ext object for Smaato 55 type bidRequestExt struct { 56 Client string `json:"client"` 57 } 58 59 // bidExt defines Bid.Ext object for Smaato 60 type bidExt struct { 61 Duration int `json:"duration"` 62 } 63 64 // videoExt defines Video.Ext object for Smaato 65 type videoExt struct { 66 Context string `json:"context,omitempty"` 67 } 68 69 // Builder builds a new instance of the Smaato adapter for the given bidder with the given config. 70 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 71 bidder := &adapter{ 72 clock: &timeutil.RealTime{}, 73 endpoint: config.Endpoint, 74 } 75 return bidder, nil 76 } 77 78 // MakeRequests makes the HTTP requests which should be made to fetch bids. 79 func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 80 if len(request.Imp) == 0 { 81 return nil, []error{&errortypes.BadInput{Message: "No impressions in bid request."}} 82 } 83 84 // set data in request that is common for all requests 85 if err := prepareCommonRequest(request); err != nil { 86 return nil, []error{err} 87 } 88 89 isVideoEntryPoint := reqInfo.PbsEntryPoint == metrics.ReqTypeVideo 90 91 if isVideoEntryPoint { 92 return adapter.makePodRequests(request) 93 } else { 94 return adapter.makeIndividualRequests(request) 95 } 96 } 97 98 // MakeBids unpacks the server's response into Bids. 99 func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 100 if response.StatusCode == http.StatusNoContent { 101 return nil, nil 102 } 103 104 if response.StatusCode != http.StatusOK { 105 return nil, []error{&errortypes.BadServerResponse{ 106 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", response.StatusCode), 107 }} 108 } 109 110 var bidResp openrtb2.BidResponse 111 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 112 return nil, []error{err} 113 } 114 115 bidResponse := adapters.NewBidderResponseWithBidsCapacity(5) 116 117 var errors []error 118 for _, seatBid := range bidResp.SeatBid { 119 for i := 0; i < len(seatBid.Bid); i++ { 120 bid := seatBid.Bid[i] 121 122 adMarkupType, err := getAdMarkupType(response, bid.AdM) 123 if err != nil { 124 errors = append(errors, err) 125 continue 126 } 127 128 bid.AdM, err = renderAdMarkup(adMarkupType, bid.AdM) 129 if err != nil { 130 errors = append(errors, err) 131 continue 132 } 133 134 bidType, err := convertAdMarkupTypeToMediaType(adMarkupType) 135 if err != nil { 136 errors = append(errors, err) 137 continue 138 } 139 140 bidVideo, err := buildBidVideo(&bid, bidType) 141 if err != nil { 142 errors = append(errors, err) 143 continue 144 } 145 146 bid.Exp = adapter.getTTLFromHeaderOrDefault(response) 147 148 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 149 Bid: &bid, 150 BidType: bidType, 151 BidVideo: bidVideo, 152 }) 153 } 154 } 155 return bidResponse, errors 156 } 157 158 func (adapter *adapter) makeIndividualRequests(request *openrtb2.BidRequest) ([]*adapters.RequestData, []error) { 159 imps := request.Imp 160 161 requests := make([]*adapters.RequestData, 0, len(imps)) 162 errors := make([]error, 0, len(imps)) 163 164 for _, imp := range imps { 165 impsByMediaType, err := splitImpressionsByMediaType(&imp) 166 if err != nil { 167 errors = append(errors, err) 168 continue 169 } 170 171 for _, impByMediaType := range impsByMediaType { 172 request.Imp = []openrtb2.Imp{impByMediaType} 173 if err := prepareIndividualRequest(request); err != nil { 174 errors = append(errors, err) 175 continue 176 } 177 178 requestData, err := adapter.makeRequest(request) 179 if err != nil { 180 errors = append(errors, err) 181 continue 182 } 183 184 requests = append(requests, requestData) 185 } 186 } 187 188 return requests, errors 189 } 190 191 func splitImpressionsByMediaType(imp *openrtb2.Imp) ([]openrtb2.Imp, error) { 192 if imp.Banner == nil && imp.Video == nil && imp.Native == nil { 193 return nil, &errortypes.BadInput{Message: "Invalid MediaType. Smaato only supports Banner, Video and Native."} 194 } 195 196 imps := make([]openrtb2.Imp, 0, 3) 197 198 if imp.Banner != nil { 199 impCopy := *imp 200 impCopy.Video = nil 201 impCopy.Native = nil 202 imps = append(imps, impCopy) 203 } 204 205 if imp.Video != nil { 206 impCopy := *imp 207 impCopy.Banner = nil 208 impCopy.Native = nil 209 imps = append(imps, impCopy) 210 } 211 212 if imp.Native != nil { 213 imp.Banner = nil 214 imp.Video = nil 215 imps = append(imps, *imp) 216 } 217 218 return imps, nil 219 } 220 221 func (adapter *adapter) makePodRequests(request *openrtb2.BidRequest) ([]*adapters.RequestData, []error) { 222 pods, orderedKeys, errors := groupImpressionsByPod(request.Imp) 223 requests := make([]*adapters.RequestData, 0, len(pods)) 224 225 for _, key := range orderedKeys { 226 request.Imp = pods[key] 227 228 if err := preparePodRequest(request); err != nil { 229 errors = append(errors, err) 230 continue 231 } 232 233 requestData, err := adapter.makeRequest(request) 234 if err != nil { 235 errors = append(errors, err) 236 continue 237 } 238 239 requests = append(requests, requestData) 240 } 241 242 return requests, errors 243 } 244 245 func (adapter *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) { 246 reqJSON, err := json.Marshal(request) 247 if err != nil { 248 return nil, err 249 } 250 251 headers := http.Header{} 252 headers.Add("Content-Type", "application/json;charset=utf-8") 253 headers.Add("Accept", "application/json") 254 255 return &adapters.RequestData{ 256 Method: "POST", 257 Uri: adapter.endpoint, 258 Body: reqJSON, 259 Headers: headers, 260 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 261 }, nil 262 } 263 264 func getAdMarkupType(response *adapters.ResponseData, adMarkup string) (adMarkupType, error) { 265 if admType := adMarkupType(response.Headers.Get("X-Smt-Adtype")); admType != "" { 266 return admType, nil 267 } else if strings.HasPrefix(adMarkup, `{"image":`) { 268 return smtAdTypeImg, nil 269 } else if strings.HasPrefix(adMarkup, `{"richmedia":`) { 270 return smtAdTypeRichmedia, nil 271 } else if strings.HasPrefix(adMarkup, `<?xml`) { 272 return smtAdTypeVideo, nil 273 } else if strings.HasPrefix(adMarkup, `{"native":`) { 274 return smtAdTypeNative, nil 275 } else { 276 return "", &errortypes.BadServerResponse{ 277 Message: fmt.Sprintf("Invalid ad markup %s.", adMarkup), 278 } 279 } 280 } 281 282 func (adapter *adapter) getTTLFromHeaderOrDefault(response *adapters.ResponseData) int64 { 283 ttl := int64(300) 284 285 if expiresAtMillis, err := strconv.ParseInt(response.Headers.Get("X-Smt-Expires"), 10, 64); err == nil { 286 nowMillis := adapter.clock.Now().UnixNano() / 1000000 287 ttl = (expiresAtMillis - nowMillis) / 1000 288 if ttl < 0 { 289 ttl = 0 290 } 291 } 292 293 return ttl 294 } 295 296 func renderAdMarkup(adMarkupType adMarkupType, adMarkup string) (string, error) { 297 switch adMarkupType { 298 case smtAdTypeImg: 299 return extractAdmImage(adMarkup) 300 case smtAdTypeRichmedia: 301 return extractAdmRichMedia(adMarkup) 302 case smtAdTypeVideo: 303 return adMarkup, nil 304 case smtAdTypeNative: 305 return extractAdmNative(adMarkup) 306 default: 307 return "", &errortypes.BadServerResponse{ 308 Message: fmt.Sprintf("Unknown markup type %s.", adMarkupType), 309 } 310 } 311 } 312 313 func convertAdMarkupTypeToMediaType(adMarkupType adMarkupType) (openrtb_ext.BidType, error) { 314 switch adMarkupType { 315 case smtAdTypeImg: 316 return openrtb_ext.BidTypeBanner, nil 317 case smtAdTypeRichmedia: 318 return openrtb_ext.BidTypeBanner, nil 319 case smtAdTypeVideo: 320 return openrtb_ext.BidTypeVideo, nil 321 case smtAdTypeNative: 322 return openrtb_ext.BidTypeNative, nil 323 default: 324 return "", &errortypes.BadServerResponse{ 325 Message: fmt.Sprintf("Unknown markup type %s.", adMarkupType), 326 } 327 } 328 } 329 330 func prepareCommonRequest(request *openrtb2.BidRequest) error { 331 if err := setUser(request); err != nil { 332 return err 333 } 334 335 if err := setSite(request); err != nil { 336 return err 337 } 338 339 setApp(request) 340 341 return setExt(request) 342 } 343 344 func prepareIndividualRequest(request *openrtb2.BidRequest) error { 345 imp := &request.Imp[0] 346 347 if err := setPublisherId(request, imp); err != nil { 348 return err 349 } 350 351 return setImpForAdspace(imp) 352 } 353 354 func preparePodRequest(request *openrtb2.BidRequest) error { 355 if len(request.Imp) < 1 { 356 return &errortypes.BadInput{Message: "No impressions in bid request."} 357 } 358 359 if err := setPublisherId(request, &request.Imp[0]); err != nil { 360 return err 361 } 362 363 return setImpForAdBreak(request.Imp) 364 } 365 366 func setUser(request *openrtb2.BidRequest) error { 367 if request.User != nil && request.User.Ext != nil { 368 var userExtRaw map[string]json.RawMessage 369 370 if err := json.Unmarshal(request.User.Ext, &userExtRaw); err != nil { 371 return &errortypes.BadInput{Message: "Invalid user.ext."} 372 } 373 374 if userExtDataRaw, present := userExtRaw["data"]; present { 375 var err error 376 var userExtData userExtData 377 378 if err = json.Unmarshal(userExtDataRaw, &userExtData); err != nil { 379 return &errortypes.BadInput{Message: "Invalid user.ext.data."} 380 } 381 382 userCopy := *request.User 383 384 if userExtData.Gender != "" { 385 userCopy.Gender = userExtData.Gender 386 } 387 388 if userExtData.Yob != 0 { 389 userCopy.Yob = userExtData.Yob 390 } 391 392 if userExtData.Keywords != "" { 393 userCopy.Keywords = userExtData.Keywords 394 } 395 396 delete(userExtRaw, "data") 397 398 if userCopy.Ext, err = json.Marshal(userExtRaw); err != nil { 399 return err 400 } 401 402 request.User = &userCopy 403 } 404 } 405 406 return nil 407 } 408 409 func setExt(request *openrtb2.BidRequest) error { 410 var err error 411 412 request.Ext, err = json.Marshal(bidRequestExt{Client: clientVersion}) 413 414 return err 415 } 416 417 func setSite(request *openrtb2.BidRequest) error { 418 if request.Site != nil { 419 siteCopy := *request.Site 420 421 if request.Site.Ext != nil { 422 var siteExt siteExt 423 424 if err := json.Unmarshal(request.Site.Ext, &siteExt); err != nil { 425 return &errortypes.BadInput{Message: "Invalid site.ext."} 426 } 427 428 siteCopy.Keywords = siteExt.Data.Keywords 429 siteCopy.Ext = nil 430 } 431 request.Site = &siteCopy 432 } 433 434 return nil 435 } 436 437 func setApp(request *openrtb2.BidRequest) { 438 if request.App != nil { 439 appCopy := *request.App 440 request.App = &appCopy 441 } 442 } 443 444 func setPublisherId(request *openrtb2.BidRequest, imp *openrtb2.Imp) error { 445 publisherID, err := jsonparser.GetString(imp.Ext, "bidder", "publisherId") 446 if err != nil { 447 return &errortypes.BadInput{Message: "Missing publisherId parameter."} 448 } 449 450 if request.Site != nil { 451 // Site is already a copy 452 request.Site.Publisher = &openrtb2.Publisher{ID: publisherID} 453 return nil 454 } else if request.App != nil { 455 // App is already a copy 456 request.App.Publisher = &openrtb2.Publisher{ID: publisherID} 457 return nil 458 } else { 459 return &errortypes.BadInput{Message: "Missing Site/App."} 460 } 461 } 462 463 func setImpForAdspace(imp *openrtb2.Imp) error { 464 adSpaceID, err := jsonparser.GetString(imp.Ext, "bidder", "adspaceId") 465 if err != nil { 466 return &errortypes.BadInput{Message: "Missing adspaceId parameter."} 467 } 468 469 impExt, err := makeImpExt(&imp.Ext) 470 if err != nil { 471 return err 472 } 473 474 if imp.Banner != nil { 475 bannerCopy, err := setBannerDimension(imp.Banner) 476 if err != nil { 477 return err 478 } 479 imp.Banner = bannerCopy 480 imp.TagID = adSpaceID 481 imp.Ext = impExt 482 return nil 483 } 484 485 if imp.Video != nil || imp.Native != nil { 486 imp.TagID = adSpaceID 487 imp.Ext = impExt 488 return nil 489 } 490 491 return nil 492 } 493 494 func setImpForAdBreak(imps []openrtb2.Imp) error { 495 if len(imps) < 1 { 496 return &errortypes.BadInput{Message: "No impressions in bid request."} 497 } 498 499 adBreakID, err := jsonparser.GetString(imps[0].Ext, "bidder", "adbreakId") 500 if err != nil { 501 return &errortypes.BadInput{Message: "Missing adbreakId parameter."} 502 } 503 504 impExt, err := makeImpExt(&imps[0].Ext) 505 if err != nil { 506 return err 507 } 508 509 for i := range imps { 510 imps[i].TagID = adBreakID 511 imps[i].Ext = nil 512 513 videoCopy := *(imps[i].Video) 514 515 videoCopy.Sequence = int8(i + 1) 516 videoCopy.Ext, _ = json.Marshal(&videoExt{Context: "adpod"}) 517 518 imps[i].Video = &videoCopy 519 } 520 521 imps[0].Ext = impExt 522 523 return nil 524 } 525 526 func makeImpExt(impExtRaw *json.RawMessage) (json.RawMessage, error) { 527 var impExt openrtb_ext.ExtImpExtraDataSmaato 528 529 if err := json.Unmarshal(*impExtRaw, &impExt); err != nil { 530 return nil, &errortypes.BadInput{Message: "Invalid imp.ext."} 531 } 532 533 if impExtSkadnRaw := impExt.Skadn; impExtSkadnRaw != nil { 534 var impExtSkadn map[string]json.RawMessage 535 536 if err := json.Unmarshal(impExtSkadnRaw, &impExtSkadn); err != nil { 537 return nil, &errortypes.BadInput{Message: "Invalid imp.ext.skadn."} 538 } 539 } 540 541 if impExtJson, err := json.Marshal(impExt); string(impExtJson) != "{}" { 542 return impExtJson, err 543 } else { 544 return nil, nil 545 } 546 } 547 548 func setBannerDimension(banner *openrtb2.Banner) (*openrtb2.Banner, error) { 549 if banner.W != nil && banner.H != nil { 550 return banner, nil 551 } 552 if len(banner.Format) == 0 { 553 return banner, &errortypes.BadInput{Message: "No sizes provided for Banner."} 554 } 555 bannerCopy := *banner 556 bannerCopy.W = ptrutil.ToPtr(banner.Format[0].W) 557 bannerCopy.H = ptrutil.ToPtr(banner.Format[0].H) 558 559 return &bannerCopy, nil 560 } 561 562 func groupImpressionsByPod(imps []openrtb2.Imp) (map[string]([]openrtb2.Imp), []string, []error) { 563 pods := make(map[string][]openrtb2.Imp) 564 orderKeys := make([]string, 0) 565 errors := make([]error, 0, len(imps)) 566 567 for _, imp := range imps { 568 if imp.Video == nil { 569 errors = append(errors, &errortypes.BadInput{Message: "Invalid MediaType. Smaato only supports Video for AdPod."}) 570 continue 571 } 572 573 pod := strings.Split(imp.ID, "_")[0] 574 if _, present := pods[pod]; !present { 575 orderKeys = append(orderKeys, pod) 576 } 577 pods[pod] = append(pods[pod], imp) 578 } 579 return pods, orderKeys, errors 580 } 581 582 func buildBidVideo(bid *openrtb2.Bid, bidType openrtb_ext.BidType) (*openrtb_ext.ExtBidPrebidVideo, error) { 583 if bidType != openrtb_ext.BidTypeVideo { 584 return nil, nil 585 } 586 587 if bid.Ext == nil { 588 return nil, nil 589 } 590 591 var primaryCategory string 592 if len(bid.Cat) > 0 { 593 primaryCategory = bid.Cat[0] 594 } 595 596 var bidExt bidExt 597 if err := json.Unmarshal(bid.Ext, &bidExt); err != nil { 598 return nil, &errortypes.BadServerResponse{Message: "Invalid bid.ext."} 599 } 600 601 return &openrtb_ext.ExtBidPrebidVideo{ 602 Duration: bidExt.Duration, 603 PrimaryCategory: primaryCategory, 604 }, nil 605 }