github.com/prebid/prebid-server/v2@v2.18.0/adapters/grid/grid.go (about) 1 package grid 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "sort" 8 "strings" 9 10 "github.com/prebid/openrtb/v20/openrtb2" 11 "github.com/prebid/prebid-server/v2/adapters" 12 "github.com/prebid/prebid-server/v2/config" 13 "github.com/prebid/prebid-server/v2/errortypes" 14 "github.com/prebid/prebid-server/v2/openrtb_ext" 15 "github.com/prebid/prebid-server/v2/util/maputil" 16 ) 17 18 type GridAdapter struct { 19 endpoint string 20 } 21 22 type GridBid struct { 23 *openrtb2.Bid 24 AdmNative json.RawMessage `json:"adm_native,omitempty"` 25 ContentType openrtb_ext.BidType `json:"content_type"` 26 } 27 28 type GridSeatBid struct { 29 *openrtb2.SeatBid 30 Bid []GridBid `json:"bid"` 31 } 32 33 type GridResponse struct { 34 *openrtb2.BidResponse 35 SeatBid []GridSeatBid `json:"seatbid,omitempty"` 36 } 37 38 type GridBidExt struct { 39 Bidder ExtBidder `json:"bidder"` 40 } 41 42 type ExtBidder struct { 43 Grid ExtBidderGrid `json:"grid"` 44 } 45 46 type ExtBidderGrid struct { 47 DemandSource string `json:"demandSource"` 48 } 49 50 type ExtImpDataAdServer struct { 51 Name string `json:"name"` 52 AdSlot string `json:"adslot"` 53 } 54 55 type ExtImpData struct { 56 PbAdslot string `json:"pbadslot,omitempty"` 57 AdServer *ExtImpDataAdServer `json:"adserver,omitempty"` 58 } 59 60 type ExtImp struct { 61 Prebid *openrtb_ext.ExtImpPrebid `json:"prebid,omitempty"` 62 Bidder json.RawMessage `json:"bidder"` 63 Data *ExtImpData `json:"data,omitempty"` 64 Gpid string `json:"gpid,omitempty"` 65 Skadn json.RawMessage `json:"skadn,omitempty"` 66 Context json.RawMessage `json:"context,omitempty"` 67 } 68 69 type KeywordSegment struct { 70 Name string `json:"name"` 71 Value string `json:"value"` 72 } 73 74 type KeywordsPublisherItem struct { 75 Name string `json:"name"` 76 Segments []KeywordSegment `json:"segments"` 77 } 78 79 type KeywordsPublisher map[string][]KeywordsPublisherItem 80 81 type Keywords map[string]KeywordsPublisher 82 83 // buildConsolidatedKeywordsReqExt builds a new request.ext json incorporating request.site.keywords, request.user.keywords, 84 // and request.imp[0].ext.keywords, and request.ext.keywords. Invalid keywords in request.imp[0].ext.keywords are not incorporated. 85 // Invalid keywords in request.ext.keywords.site and request.ext.keywords.user are dropped. 86 func buildConsolidatedKeywordsReqExt(openRTBUser, openRTBSite string, firstImpExt, requestExt json.RawMessage) (json.RawMessage, error) { 87 // unmarshal ext to object map 88 requestExtMap := parseExtToMap(requestExt) 89 firstImpExtMap := parseExtToMap(firstImpExt) 90 // extract `keywords` field 91 requestExtKeywordsMap := extractKeywordsMap(requestExtMap) 92 firstImpExtKeywordsMap := extractBidderKeywordsMap(firstImpExtMap) 93 // parse + merge keywords 94 keywords := parseKeywordsFromMap(requestExtKeywordsMap) // request.ext.keywords 95 mergeKeywords(keywords, parseKeywordsFromMap(firstImpExtKeywordsMap)) // request.imp[0].ext.bidder.keywords 96 mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBUser, "user")) // request.user.keywords 97 mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBSite, "site")) // request.site.keywords 98 99 // overlay site + user keywords 100 if site, exists := keywords["site"]; exists && len(site) > 0 { 101 requestExtKeywordsMap["site"] = site 102 } else { 103 delete(requestExtKeywordsMap, "site") 104 } 105 if user, exists := keywords["user"]; exists && len(user) > 0 { 106 requestExtKeywordsMap["user"] = user 107 } else { 108 delete(requestExtKeywordsMap, "user") 109 } 110 // reconcile keywords with request.ext 111 if len(requestExtKeywordsMap) > 0 { 112 requestExtMap["keywords"] = requestExtKeywordsMap 113 } else { 114 delete(requestExtMap, "keywords") 115 } 116 // marshal final result 117 if len(requestExtMap) > 0 { 118 return json.Marshal(requestExtMap) 119 } 120 return nil, nil 121 } 122 func parseExtToMap(ext json.RawMessage) map[string]interface{} { 123 var root map[string]interface{} 124 if err := json.Unmarshal(ext, &root); err != nil { 125 return make(map[string]interface{}) 126 } 127 return root 128 } 129 func extractKeywordsMap(ext map[string]interface{}) map[string]interface{} { 130 if keywords, exists := maputil.ReadEmbeddedMap(ext, "keywords"); exists { 131 return keywords 132 } 133 return make(map[string]interface{}) 134 } 135 func extractBidderKeywordsMap(ext map[string]interface{}) map[string]interface{} { 136 if bidder, exists := maputil.ReadEmbeddedMap(ext, "bidder"); exists { 137 return extractKeywordsMap(bidder) 138 } 139 return make(map[string]interface{}) 140 } 141 func parseKeywordsFromMap(extKeywords map[string]interface{}) Keywords { 142 keywords := make(Keywords) 143 for k, v := range extKeywords { 144 // keywords may only be provided in the site and user sections 145 if k != "site" && k != "user" { 146 continue 147 } 148 // the site or user sections must be an object 149 if section, ok := v.(map[string]interface{}); ok { 150 keywords[k] = parseKeywordsFromSection(section) 151 } 152 } 153 return keywords 154 } 155 func parseKeywordsFromSection(section map[string]interface{}) KeywordsPublisher { 156 keywordsPublishers := make(KeywordsPublisher) 157 for publisherKey, publisherValue := range section { 158 // publisher value must be a slice 159 publisherValueSlice, ok := publisherValue.([]interface{}) 160 if !ok { 161 continue 162 } 163 for _, publisherValueItem := range publisherValueSlice { 164 // item must be an object 165 publisherItem, ok := publisherValueItem.(map[string]interface{}) 166 if !ok { 167 continue 168 } 169 // publisher item must have a name 170 publisherName, ok := maputil.ReadEmbeddedString(publisherItem, "name") 171 if !ok { 172 continue 173 } 174 var segments []KeywordSegment 175 // extract valid segments 176 if segmentsSlice, exists := maputil.ReadEmbeddedSlice(publisherItem, "segments"); exists { 177 for _, segment := range segmentsSlice { 178 if segmentMap, ok := segment.(map[string]interface{}); ok { 179 name, hasName := maputil.ReadEmbeddedString(segmentMap, "name") 180 value, hasValue := maputil.ReadEmbeddedString(segmentMap, "value") 181 if hasName && hasValue { 182 segments = append(segments, KeywordSegment{Name: name, Value: value}) 183 } 184 } 185 } 186 } 187 // ensure consistent ordering for publisher item map 188 publisherItemKeys := make([]string, 0, len(publisherItem)) 189 for v := range publisherItem { 190 publisherItemKeys = append(publisherItemKeys, v) 191 } 192 sort.Strings(publisherItemKeys) 193 // compose compatible alternate segment format 194 for _, potentialSegmentName := range publisherItemKeys { 195 potentialSegmentValues := publisherItem[potentialSegmentName] 196 // values must be an array 197 if valuesSlice, ok := potentialSegmentValues.([]interface{}); ok { 198 for _, value := range valuesSlice { 199 if valueAsString, ok := value.(string); ok { 200 segments = append(segments, KeywordSegment{Name: potentialSegmentName, Value: valueAsString}) 201 } 202 } 203 } 204 } 205 if len(segments) > 0 { 206 keywordsPublishers[publisherKey] = append(keywordsPublishers[publisherKey], KeywordsPublisherItem{Name: publisherName, Segments: segments}) 207 } 208 } 209 } 210 return keywordsPublishers 211 } 212 func parseKeywordsFromOpenRTB(keywords, section string) Keywords { 213 keywordsSplit := strings.Split(keywords, ",") 214 segments := make([]KeywordSegment, 0, len(keywordsSplit)) 215 for _, v := range keywordsSplit { 216 if v != "" { 217 segments = append(segments, KeywordSegment{Name: "keywords", Value: v}) 218 } 219 } 220 if len(segments) > 0 { 221 return map[string]KeywordsPublisher{section: map[string][]KeywordsPublisherItem{"ortb2": {{Name: "keywords", Segments: segments}}}} 222 } 223 return make(Keywords) 224 } 225 func mergeKeywords(a, b Keywords) { 226 for key, values := range b { 227 if _, sectionExists := a[key]; !sectionExists { 228 a[key] = KeywordsPublisher{} 229 } 230 for publisherKey, publisherValues := range values { 231 a[key][publisherKey] = append(publisherValues, a[key][publisherKey]...) 232 } 233 } 234 } 235 236 func setImpExtKeywords(request *openrtb2.BidRequest) error { 237 userKeywords := "" 238 if request.User != nil { 239 userKeywords = request.User.Keywords 240 } 241 siteKeywords := "" 242 if request.Site != nil { 243 siteKeywords = request.Site.Keywords 244 } 245 var err error 246 request.Ext, err = buildConsolidatedKeywordsReqExt(userKeywords, siteKeywords, request.Imp[0].Ext, request.Ext) 247 return err 248 } 249 250 func processImp(imp *openrtb2.Imp) error { 251 // get the grid extension 252 var ext adapters.ExtImpBidder 253 var gridExt openrtb_ext.ExtImpGrid 254 if err := json.Unmarshal(imp.Ext, &ext); err != nil { 255 return err 256 } 257 if err := json.Unmarshal(ext.Bidder, &gridExt); err != nil { 258 return err 259 } 260 261 if gridExt.Uid == 0 { 262 err := &errortypes.BadInput{ 263 Message: "uid is empty", 264 } 265 return err 266 } 267 // no error 268 return nil 269 } 270 271 func setImpExtData(imp openrtb2.Imp) openrtb2.Imp { 272 var ext ExtImp 273 if err := json.Unmarshal(imp.Ext, &ext); err != nil { 274 return imp 275 } 276 if ext.Data != nil && ext.Data.AdServer != nil && ext.Data.AdServer.AdSlot != "" { 277 ext.Gpid = ext.Data.AdServer.AdSlot 278 extJSON, err := json.Marshal(ext) 279 if err == nil { 280 imp.Ext = extJSON 281 } 282 } 283 return imp 284 } 285 286 func fixNative(req json.RawMessage) (json.RawMessage, error) { 287 var gridReq map[string]interface{} 288 var parsedRequest map[string]interface{} 289 290 if err := json.Unmarshal(req, &gridReq); err != nil { 291 return req, nil 292 } 293 if imps, exists := maputil.ReadEmbeddedSlice(gridReq, "imp"); exists { 294 for _, imp := range imps { 295 if gridImp, ok := imp.(map[string]interface{}); ok { 296 native, hasNative := maputil.ReadEmbeddedMap(gridImp, "native") 297 if hasNative { 298 request, hasRequest := maputil.ReadEmbeddedString(native, "request") 299 if hasRequest { 300 delete(native, "request") 301 if err := json.Unmarshal([]byte(request), &parsedRequest); err == nil { 302 native["request_native"] = parsedRequest 303 } else { 304 native["request_native"] = request 305 } 306 } 307 } 308 } 309 } 310 } 311 312 return json.Marshal(gridReq) 313 } 314 315 // MakeRequests makes the HTTP requests which should be made to fetch bids. 316 func (a *GridAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 317 var errors = make([]error, 0) 318 319 // this will contain all the valid impressions 320 var validImps []openrtb2.Imp 321 // pre-process the imps 322 for _, imp := range request.Imp { 323 if err := processImp(&imp); err == nil { 324 validImps = append(validImps, setImpExtData(imp)) 325 } else { 326 errors = append(errors, err) 327 } 328 } 329 if len(validImps) == 0 { 330 err := &errortypes.BadInput{ 331 Message: "No valid impressions for grid", 332 } 333 errors = append(errors, err) 334 return nil, errors 335 } 336 337 if err := setImpExtKeywords(request); err != nil { 338 errors = append(errors, err) 339 return nil, errors 340 } 341 342 request.Imp = validImps 343 344 reqJSON, err := json.Marshal(request) 345 346 if err != nil { 347 errors = append(errors, err) 348 return nil, errors 349 } 350 351 fixedReqJSON, err := fixNative(reqJSON) 352 353 if err != nil { 354 errors = append(errors, err) 355 return nil, errors 356 } 357 358 headers := http.Header{} 359 headers.Add("Content-Type", "application/json;charset=utf-8") 360 361 return []*adapters.RequestData{{ 362 Method: "POST", 363 Uri: a.endpoint, 364 Body: fixedReqJSON, 365 Headers: headers, 366 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 367 }}, errors 368 } 369 370 // MakeBids unpacks the server's response into Bids. 371 func (a *GridAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 372 if response.StatusCode == http.StatusNoContent { 373 return nil, nil 374 } 375 376 if response.StatusCode == http.StatusBadRequest { 377 return nil, []error{&errortypes.BadInput{ 378 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 379 }} 380 } 381 382 if response.StatusCode != http.StatusOK { 383 return nil, []error{&errortypes.BadServerResponse{ 384 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 385 }} 386 } 387 388 var bidResp GridResponse 389 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 390 return nil, []error{err} 391 } 392 393 bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) 394 395 for _, sb := range bidResp.SeatBid { 396 for i := range sb.Bid { 397 bidMeta, err := getBidMeta(sb.Bid[i].Ext) 398 bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp, sb.Bid[i]) 399 if sb.Bid[i].AdmNative != nil && sb.Bid[i].AdM == "" { 400 if bytes, err := json.Marshal(sb.Bid[i].AdmNative); err == nil { 401 sb.Bid[i].AdM = string(bytes) 402 } 403 } 404 if err != nil { 405 return nil, []error{err} 406 } 407 408 openrtb2Bid := sb.Bid[i].Bid 409 410 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 411 Bid: openrtb2Bid, 412 BidType: bidType, 413 BidMeta: bidMeta, 414 }) 415 } 416 } 417 return bidResponse, nil 418 419 } 420 421 // Builder builds a new instance of the Grid adapter for the given bidder with the given config. 422 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 423 bidder := &GridAdapter{ 424 endpoint: config.Endpoint, 425 } 426 return bidder, nil 427 } 428 429 func getBidMeta(ext json.RawMessage) (*openrtb_ext.ExtBidPrebidMeta, error) { 430 var bidExt GridBidExt 431 432 if err := json.Unmarshal(ext, &bidExt); err != nil { 433 return nil, err 434 } 435 var bidMeta *openrtb_ext.ExtBidPrebidMeta 436 if bidExt.Bidder.Grid.DemandSource != "" { 437 bidMeta = &openrtb_ext.ExtBidPrebidMeta{ 438 NetworkName: bidExt.Bidder.Grid.DemandSource, 439 } 440 } 441 return bidMeta, nil 442 } 443 444 func getMediaTypeForImp(impID string, imps []openrtb2.Imp, bidWithType GridBid) (openrtb_ext.BidType, error) { 445 if bidWithType.ContentType != "" { 446 return bidWithType.ContentType, nil 447 } else { 448 for _, imp := range imps { 449 if imp.ID == impID { 450 if imp.Banner != nil { 451 return openrtb_ext.BidTypeBanner, nil 452 } 453 454 if imp.Video != nil { 455 return openrtb_ext.BidTypeVideo, nil 456 } 457 458 if imp.Native != nil { 459 return openrtb_ext.BidTypeNative, nil 460 } 461 462 return "", &errortypes.BadServerResponse{ 463 Message: fmt.Sprintf("Unknown impression type for ID: \"%s\"", impID), 464 } 465 } 466 } 467 } 468 469 // This shouldnt happen. Lets handle it just incase by returning an error. 470 return "", &errortypes.BadServerResponse{ 471 Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", impID), 472 } 473 }