github.com/prebid/prebid-server@v0.275.0/adapters/ix/ix.go (about) 1 package ix 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "sort" 8 "strings" 9 10 "github.com/prebid/prebid-server/adapters" 11 "github.com/prebid/prebid-server/config" 12 "github.com/prebid/prebid-server/errortypes" 13 "github.com/prebid/prebid-server/openrtb_ext" 14 "github.com/prebid/prebid-server/version" 15 16 "github.com/prebid/openrtb/v19/native1" 17 native1response "github.com/prebid/openrtb/v19/native1/response" 18 "github.com/prebid/openrtb/v19/openrtb2" 19 ) 20 21 type IxAdapter struct { 22 URI string 23 } 24 25 type ExtRequest struct { 26 Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"` 27 SChain *openrtb2.SupplyChain `json:"schain,omitempty"` 28 IxDiag *IxDiag `json:"ixdiag,omitempty"` 29 } 30 31 type IxDiag struct { 32 PbsV string `json:"pbsv,omitempty"` 33 PbjsV string `json:"pbjsv,omitempty"` 34 MultipleSiteIds string `json:"multipleSiteIds,omitempty"` 35 } 36 37 func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 38 requests := make([]*adapters.RequestData, 0, len(request.Imp)) 39 errs := make([]error, 0) 40 41 headers := http.Header{ 42 "Content-Type": {"application/json;charset=utf-8"}, 43 "Accept": {"application/json"}} 44 45 uniqueSiteIDs := make(map[string]struct{}) 46 filteredImps := make([]openrtb2.Imp, 0, len(request.Imp)) 47 requestCopy := *request 48 49 ixDiag := &IxDiag{} 50 51 for _, imp := range requestCopy.Imp { 52 var err error 53 ixExt, err := unmarshalToIxExt(&imp) 54 55 if err != nil { 56 errs = append(errs, err) 57 continue 58 } 59 60 if err = parseSiteId(ixExt, uniqueSiteIDs); err != nil { 61 errs = append(errs, err) 62 continue 63 } 64 65 if err := moveSid(&imp, ixExt); err != nil { 66 errs = append(errs, err) 67 } 68 69 if imp.Banner != nil { 70 bannerCopy := *imp.Banner 71 72 if len(bannerCopy.Format) == 0 && bannerCopy.W != nil && bannerCopy.H != nil { 73 bannerCopy.Format = []openrtb2.Format{{W: *bannerCopy.W, H: *bannerCopy.H}} 74 } 75 76 if len(bannerCopy.Format) == 1 { 77 bannerCopy.W = openrtb2.Int64Ptr(bannerCopy.Format[0].W) 78 bannerCopy.H = openrtb2.Int64Ptr(bannerCopy.Format[0].H) 79 } 80 imp.Banner = &bannerCopy 81 } 82 filteredImps = append(filteredImps, imp) 83 } 84 requestCopy.Imp = filteredImps 85 86 setPublisherId(&requestCopy, uniqueSiteIDs, ixDiag) 87 88 err := setIxDiagIntoExtRequest(&requestCopy, ixDiag) 89 if err != nil { 90 errs = append(errs, err) 91 } 92 93 if len(requestCopy.Imp) != 0 { 94 if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil { 95 requests = append(requests, requestData) 96 } else { 97 errs = append(errs, err) 98 } 99 } 100 101 return requests, errs 102 } 103 104 func setPublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[string]struct{}, ixDiag *IxDiag) { 105 siteIDs := make([]string, 0, len(uniqueSiteIDs)) 106 for key := range uniqueSiteIDs { 107 siteIDs = append(siteIDs, key) 108 } 109 if requestCopy.Site != nil { 110 site := *requestCopy.Site 111 if site.Publisher == nil { 112 site.Publisher = &openrtb2.Publisher{} 113 } else { 114 publisher := *site.Publisher 115 site.Publisher = &publisher 116 } 117 if len(siteIDs) == 1 { 118 site.Publisher.ID = siteIDs[0] 119 } 120 requestCopy.Site = &site 121 } 122 123 if requestCopy.App != nil { 124 app := *requestCopy.App 125 126 if app.Publisher == nil { 127 app.Publisher = &openrtb2.Publisher{} 128 } else { 129 publisher := *app.Publisher 130 app.Publisher = &publisher 131 } 132 if len(siteIDs) == 1 { 133 app.Publisher.ID = siteIDs[0] 134 } 135 requestCopy.App = &app 136 } 137 138 if len(siteIDs) > 1 { 139 // Sorting siteIDs for predictable output as Go maps don't guarantee order 140 sort.Strings(siteIDs) 141 multipleSiteIDs := strings.Join(siteIDs, ", ") 142 ixDiag.MultipleSiteIds = multipleSiteIDs 143 } 144 } 145 146 func unmarshalToIxExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpIx, error) { 147 var bidderExt adapters.ExtImpBidder 148 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 149 return nil, err 150 } 151 152 var ixExt openrtb_ext.ExtImpIx 153 if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { 154 return nil, err 155 } 156 157 return &ixExt, nil 158 } 159 160 func parseSiteId(ixExt *openrtb_ext.ExtImpIx, uniqueSiteIDs map[string]struct{}) error { 161 if ixExt == nil { 162 return fmt.Errorf("Nil Ix Ext") 163 } 164 if ixExt.SiteId != "" { 165 uniqueSiteIDs[ixExt.SiteId] = struct{}{} 166 } 167 return nil 168 } 169 170 func createRequestData(a *IxAdapter, request *openrtb2.BidRequest, headers *http.Header) (*adapters.RequestData, error) { 171 body, err := json.Marshal(request) 172 return &adapters.RequestData{ 173 Method: "POST", 174 Uri: a.URI, 175 Body: body, 176 Headers: *headers, 177 }, err 178 } 179 180 func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 181 switch { 182 case response.StatusCode == http.StatusNoContent: 183 return nil, nil 184 case response.StatusCode == http.StatusBadRequest: 185 return nil, []error{&errortypes.BadInput{ 186 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 187 }} 188 case response.StatusCode != http.StatusOK: 189 return nil, []error{&errortypes.BadServerResponse{ 190 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 191 }} 192 } 193 194 var bidResponse openrtb2.BidResponse 195 if err := json.Unmarshal(response.Body, &bidResponse); err != nil { 196 return nil, []error{&errortypes.BadServerResponse{ 197 Message: fmt.Sprintf("JSON parsing error: %v", err), 198 }} 199 } 200 201 // Store media type per impression in a map for later use to set in bid.ext.prebid.type 202 // Won't work for multiple bid case with a multi-format ad unit. We expect to get type from exchange on such case. 203 impMediaTypeReq := map[string]openrtb_ext.BidType{} 204 for _, imp := range internalRequest.Imp { 205 if imp.Banner != nil { 206 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeBanner 207 } else if imp.Video != nil { 208 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeVideo 209 } else if imp.Native != nil { 210 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeNative 211 } else if imp.Audio != nil { 212 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeAudio 213 } 214 } 215 216 // capacity 0 will make channel unbuffered 217 bidderResponse := adapters.NewBidderResponseWithBidsCapacity(0) 218 bidderResponse.Currency = bidResponse.Cur 219 220 var errs []error 221 222 for _, seatBid := range bidResponse.SeatBid { 223 for i := range seatBid.Bid { 224 bid := seatBid.Bid[i] 225 226 bidType, err := getMediaTypeForBid(bid, impMediaTypeReq) 227 if err != nil { 228 errs = append(errs, err) 229 continue 230 } 231 232 var bidExtVideo *openrtb_ext.ExtBidPrebidVideo 233 var bidExt openrtb_ext.ExtBid 234 if bidType == openrtb_ext.BidTypeVideo { 235 unmarshalExtErr := json.Unmarshal(bid.Ext, &bidExt) 236 if unmarshalExtErr == nil && bidExt.Prebid != nil && bidExt.Prebid.Video != nil { 237 bidExtVideo = &openrtb_ext.ExtBidPrebidVideo{ 238 Duration: bidExt.Prebid.Video.Duration, 239 } 240 if len(bid.Cat) == 0 { 241 bid.Cat = []string{bidExt.Prebid.Video.PrimaryCategory} 242 } 243 } 244 } 245 246 var bidNative1v1 *Native11Wrapper 247 if bidType == openrtb_ext.BidTypeNative { 248 err := json.Unmarshal([]byte(bid.AdM), &bidNative1v1) 249 if err == nil && len(bidNative1v1.Native.EventTrackers) > 0 { 250 mergeNativeImpTrackers(&bidNative1v1.Native) 251 if json, err := marshalJsonWithoutUnicode(bidNative1v1); err == nil { 252 bid.AdM = string(json) 253 } 254 } 255 } 256 257 var bidNative1v2 *native1response.Response 258 if bidType == openrtb_ext.BidTypeNative { 259 err := json.Unmarshal([]byte(bid.AdM), &bidNative1v2) 260 if err == nil && len(bidNative1v2.EventTrackers) > 0 { 261 mergeNativeImpTrackers(bidNative1v2) 262 if json, err := marshalJsonWithoutUnicode(bidNative1v2); err == nil { 263 bid.AdM = string(json) 264 } 265 } 266 } 267 268 bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ 269 Bid: &bid, 270 BidType: bidType, 271 BidVideo: bidExtVideo, 272 }) 273 } 274 } 275 276 return bidderResponse, errs 277 } 278 279 func getMediaTypeForBid(bid openrtb2.Bid, impMediaTypeReq map[string]openrtb_ext.BidType) (openrtb_ext.BidType, error) { 280 switch bid.MType { 281 case openrtb2.MarkupBanner: 282 return openrtb_ext.BidTypeBanner, nil 283 case openrtb2.MarkupVideo: 284 return openrtb_ext.BidTypeVideo, nil 285 case openrtb2.MarkupAudio: 286 return openrtb_ext.BidTypeAudio, nil 287 case openrtb2.MarkupNative: 288 return openrtb_ext.BidTypeNative, nil 289 } 290 291 if bid.Ext != nil { 292 var bidExt openrtb_ext.ExtBid 293 err := json.Unmarshal(bid.Ext, &bidExt) 294 if err == nil && bidExt.Prebid != nil { 295 prebidType := string(bidExt.Prebid.Type) 296 if prebidType != "" { 297 return openrtb_ext.ParseBidType(prebidType) 298 } 299 } 300 } 301 302 if bidType, ok := impMediaTypeReq[bid.ImpID]; ok { 303 return bidType, nil 304 } else { 305 return "", fmt.Errorf("unmatched impression id: %s", bid.ImpID) 306 } 307 } 308 309 // Builder builds a new instance of the Ix adapter for the given bidder with the given config. 310 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 311 bidder := &IxAdapter{ 312 URI: config.Endpoint, 313 } 314 return bidder, nil 315 } 316 317 // native 1.2 to 1.1 tracker compatibility handling 318 319 type Native11Wrapper struct { 320 Native native1response.Response `json:"native,omitempty"` 321 } 322 323 func mergeNativeImpTrackers(bidNative *native1response.Response) { 324 325 // create unique list of imp pixels urls from `imptrackers` and `eventtrackers` 326 uniqueImpPixels := map[string]struct{}{} 327 for _, v := range bidNative.ImpTrackers { 328 uniqueImpPixels[v] = struct{}{} 329 } 330 331 for _, v := range bidNative.EventTrackers { 332 if v.Event == native1.EventTypeImpression && v.Method == native1.EventTrackingMethodImage { 333 uniqueImpPixels[v.URL] = struct{}{} 334 } 335 } 336 337 // rewrite `imptrackers` with new deduped list of imp pixels 338 bidNative.ImpTrackers = make([]string, 0) 339 for k := range uniqueImpPixels { 340 bidNative.ImpTrackers = append(bidNative.ImpTrackers, k) 341 } 342 343 // sort so tests pass correctly 344 sort.Strings(bidNative.ImpTrackers) 345 } 346 347 func marshalJsonWithoutUnicode(v interface{}) (string, error) { 348 // json.Marshal uses HTMLEscape for strings inside JSON which affects URLs 349 // this is a problem with Native responses that embed JSON within JSON 350 // a custom encoder can be used to disable this encoding. 351 // https://pkg.go.dev/encoding/json#Marshal 352 // https://pkg.go.dev/encoding/json#Encoder.SetEscapeHTML 353 sb := &strings.Builder{} 354 encoder := json.NewEncoder(sb) 355 encoder.SetEscapeHTML(false) 356 if err := encoder.Encode(v); err != nil { 357 return "", err 358 } 359 // json.Encode also writes a newline, need to remove 360 // https://pkg.go.dev/encoding/json#Encoder.Encode 361 return strings.TrimSuffix(sb.String(), "\n"), nil 362 } 363 364 func setIxDiagIntoExtRequest(request *openrtb2.BidRequest, ixDiag *IxDiag) error { 365 extRequest := &ExtRequest{} 366 if request.Ext != nil { 367 if err := json.Unmarshal(request.Ext, &extRequest); err != nil { 368 return err 369 } 370 } 371 372 if extRequest.Prebid != nil && extRequest.Prebid.Channel != nil { 373 ixDiag.PbjsV = extRequest.Prebid.Channel.Version 374 } 375 // Slice commit hash out of version 376 if strings.Contains(version.Ver, "-") { 377 ixDiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")] 378 } else if version.Ver != "" { 379 ixDiag.PbsV = version.Ver 380 } 381 382 // Only set request.ext if ixDiag is not empty 383 if *ixDiag != (IxDiag{}) { 384 extRequest := &ExtRequest{} 385 if request.Ext != nil { 386 if err := json.Unmarshal(request.Ext, &extRequest); err != nil { 387 return err 388 } 389 } 390 extRequest.IxDiag = ixDiag 391 extRequestJson, err := json.Marshal(extRequest) 392 if err != nil { 393 return err 394 } 395 request.Ext = extRequestJson 396 } 397 return nil 398 } 399 400 // moves sid from imp[].ext.bidder.sid to imp[].ext.sid 401 func moveSid(imp *openrtb2.Imp, ixExt *openrtb_ext.ExtImpIx) error { 402 if ixExt == nil { 403 return fmt.Errorf("Nil Ix Ext") 404 } 405 406 if ixExt.Sid != "" { 407 var m map[string]interface{} 408 if err := json.Unmarshal(imp.Ext, &m); err != nil { 409 return err 410 } 411 m["sid"] = ixExt.Sid 412 ext, err := json.Marshal(m) 413 if err != nil { 414 return err 415 } 416 imp.Ext = ext 417 } 418 return nil 419 }