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