github.com/prebid/prebid-server/v2@v2.18.0/adapters/improvedigital/improvedigital.go (about) 1 package improvedigital 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "regexp" 8 "strconv" 9 "strings" 10 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/openrtb_ext" 16 ) 17 18 const ( 19 isRewardedInventory = "is_rewarded_inventory" 20 stateRewardedInventoryEnable = "1" 21 consentProvidersSettingsInputKey = "ConsentedProvidersSettings" 22 consentProvidersSettingsOutKey = "consented_providers_settings" 23 consentedProvidersKey = "consented_providers" 24 publisherEndpointParam = "{PublisherId}" 25 ) 26 27 type ImprovedigitalAdapter struct { 28 endpoint string 29 } 30 31 // BidExt represents Improved Digital bid extension with line item ID and buying type values 32 type BidExt struct { 33 Improvedigital struct { 34 LineItemID int `json:"line_item_id"` 35 BuyingType string `json:"buying_type"` 36 } 37 } 38 39 // ImpExtBidder represents Improved Digital bid extension with Publisher ID 40 type ImpExtBidder struct { 41 Bidder struct { 42 PublisherID int `json:"publisherId"` 43 } 44 } 45 46 var dealDetectionRegEx = regexp.MustCompile("(classic|deal)") 47 48 // MakeRequests makes the HTTP requests which should be made to fetch bids. 49 func (a *ImprovedigitalAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 50 numRequests := len(request.Imp) 51 errors := make([]error, 0) 52 adapterRequests := make([]*adapters.RequestData, 0, numRequests) 53 54 // Split multi-imp request into multiple ad server requests. SRA is currently not recommended. 55 for i := 0; i < numRequests; i++ { 56 if adapterReq, err := a.makeRequest(*request, request.Imp[i]); err == nil { 57 adapterRequests = append(adapterRequests, adapterReq) 58 } else { 59 errors = append(errors, err) 60 } 61 } 62 63 return adapterRequests, errors 64 } 65 66 func (a *ImprovedigitalAdapter) makeRequest(request openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) { 67 // Handle Rewarded Inventory 68 impExt, err := getImpExtWithRewardedInventory(imp) 69 if err != nil { 70 return nil, err 71 } 72 if impExt != nil { 73 imp.Ext = impExt 74 } 75 76 request.Imp = []openrtb2.Imp{imp} 77 78 userExtAddtlConsent, err := a.getAdditionalConsentProvidersUserExt(request) 79 if err != nil { 80 return nil, err 81 } 82 83 if len(userExtAddtlConsent) > 0 { 84 userCopy := *request.User 85 userCopy.Ext = userExtAddtlConsent 86 request.User = &userCopy 87 } 88 89 reqJSON, err := json.Marshal(request) 90 if err != nil { 91 return nil, err 92 } 93 94 headers := http.Header{} 95 headers.Add("Content-Type", "application/json;charset=utf-8") 96 97 return &adapters.RequestData{ 98 Method: "POST", 99 Uri: a.buildEndpointURL(imp), 100 Body: reqJSON, 101 Headers: headers, 102 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 103 }, nil 104 } 105 106 // MakeBids unpacks the server's response into Bids. 107 func (a *ImprovedigitalAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 108 if response.StatusCode == http.StatusNoContent { 109 return nil, nil 110 } 111 112 if response.StatusCode == http.StatusBadRequest { 113 return nil, []error{&errortypes.BadInput{ 114 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 115 }} 116 } 117 118 if response.StatusCode != http.StatusOK { 119 return nil, []error{&errortypes.BadServerResponse{ 120 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 121 }} 122 } 123 124 var bidResp openrtb2.BidResponse 125 var impMap = make(map[string]openrtb2.Imp) 126 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 127 return nil, []error{err} 128 } 129 130 if len(bidResp.SeatBid) == 0 { 131 return nil, nil 132 } 133 134 if len(bidResp.SeatBid) > 1 { 135 return nil, []error{&errortypes.BadServerResponse{ 136 Message: fmt.Sprintf("Unexpected SeatBid! Must be only one but have: %d", len(bidResp.SeatBid)), 137 }} 138 } 139 140 seatBid := bidResp.SeatBid[0] 141 if len(seatBid.Bid) == 0 { 142 return nil, nil 143 } 144 145 bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seatBid.Bid)) 146 bidResponse.Currency = bidResp.Cur 147 148 for i := range internalRequest.Imp { 149 impMap[internalRequest.Imp[i].ID] = internalRequest.Imp[i] 150 } 151 152 for i := range seatBid.Bid { 153 bid := seatBid.Bid[i] 154 155 bidType, err := getBidType(bid, impMap) 156 if err != nil { 157 return nil, []error{err} 158 } 159 160 if bid.Ext != nil { 161 var bidExt BidExt 162 err = json.Unmarshal(bid.Ext, &bidExt) 163 if err != nil { 164 return nil, []error{err} 165 } 166 167 bidExtImprovedigital := bidExt.Improvedigital 168 if bidExtImprovedigital.LineItemID != 0 && dealDetectionRegEx.MatchString(bidExtImprovedigital.BuyingType) { 169 bid.DealID = strconv.Itoa(bidExtImprovedigital.LineItemID) 170 } 171 } 172 173 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 174 Bid: &bid, 175 BidType: bidType, 176 }) 177 } 178 return bidResponse, nil 179 } 180 181 // Builder builds a new instance of the Improvedigital adapter for the given bidder with the given config. 182 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 183 bidder := &ImprovedigitalAdapter{ 184 endpoint: config.Endpoint, 185 } 186 return bidder, nil 187 } 188 189 func getBidType(bid openrtb2.Bid, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) { 190 // there must be a matching imp against bid.ImpID 191 imp, found := impMap[bid.ImpID] 192 if !found { 193 return "", &errortypes.BadServerResponse{ 194 Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", bid.ImpID), 195 } 196 } 197 198 // if MType is not set in server response, try to determine it 199 if bid.MType == 0 { 200 if !isMultiFormatImp(imp) { 201 // Not a bid for multi format impression. So, determine MType from impression 202 if imp.Banner != nil { 203 bid.MType = openrtb2.MarkupBanner 204 } else if imp.Video != nil { 205 bid.MType = openrtb2.MarkupVideo 206 } else if imp.Audio != nil { 207 bid.MType = openrtb2.MarkupAudio 208 } else if imp.Native != nil { 209 bid.MType = openrtb2.MarkupNative 210 } else { // This should not happen. 211 // Let's handle it just in case by returning an error. 212 return "", &errortypes.BadServerResponse{ 213 Message: fmt.Sprintf("Could not determine MType from impression with ID: \"%s\"", bid.ImpID), 214 } 215 } 216 } else { 217 return "", &errortypes.BadServerResponse{ 218 Message: fmt.Sprintf("Bid must have non-zero MType for multi format impression with ID: \"%s\"", bid.ImpID), 219 } 220 } 221 } 222 223 // map MType to BidType 224 switch bid.MType { 225 case openrtb2.MarkupBanner: 226 return openrtb_ext.BidTypeBanner, nil 227 case openrtb2.MarkupVideo: 228 return openrtb_ext.BidTypeVideo, nil 229 case openrtb2.MarkupAudio: 230 return openrtb_ext.BidTypeAudio, nil 231 case openrtb2.MarkupNative: 232 return openrtb_ext.BidTypeNative, nil 233 default: 234 // This shouldn't happen. Let's handle it just in case by returning an error. 235 return "", &errortypes.BadServerResponse{ 236 Message: fmt.Sprintf("Unsupported MType %d for impression with ID: \"%s\"", bid.MType, bid.ImpID), 237 } 238 } 239 } 240 241 func isMultiFormatImp(imp openrtb2.Imp) bool { 242 formatCount := 0 243 if imp.Banner != nil { 244 formatCount++ 245 } 246 if imp.Video != nil { 247 formatCount++ 248 } 249 if imp.Audio != nil { 250 formatCount++ 251 } 252 if imp.Native != nil { 253 formatCount++ 254 } 255 return formatCount > 1 256 } 257 258 // This method responsible to clone request and convert additional consent providers string to array when additional consent provider found 259 func (a *ImprovedigitalAdapter) getAdditionalConsentProvidersUserExt(request openrtb2.BidRequest) ([]byte, error) { 260 var cpStr string 261 262 // If user/user.ext not defined, no need to parse additional consent 263 if request.User == nil || request.User.Ext == nil { 264 return nil, nil 265 } 266 267 // Start validating additional consent 268 // Check key exist user.ext.ConsentedProvidersSettings 269 var userExtMap = make(map[string]json.RawMessage) 270 if err := json.Unmarshal(request.User.Ext, &userExtMap); err != nil { 271 return nil, err 272 } 273 274 cpsMapValue, cpsJSONFound := userExtMap[consentProvidersSettingsInputKey] 275 if !cpsJSONFound { 276 return nil, nil 277 } 278 279 // Check key exist user.ext.ConsentedProvidersSettings.consented_providers 280 var cpMap = make(map[string]json.RawMessage) 281 if err := json.Unmarshal(cpsMapValue, &cpMap); err != nil { 282 return nil, err 283 } 284 285 cpMapValue, cpJSONFound := cpMap[consentedProvidersKey] 286 if !cpJSONFound { 287 return nil, nil 288 } 289 // End validating additional consent 290 291 // Trim enclosing quotes after casting json.RawMessage to string 292 consentStr := strings.Trim((string)(cpMapValue), "\"") 293 // Split by ~ and take only the second string (if exists) as the consented providers spec 294 var consentStrParts = strings.Split(consentStr, "~") 295 if len(consentStrParts) < 2 { 296 return nil, nil 297 } 298 cpStr = strings.TrimSpace(consentStrParts[1]) 299 if len(cpStr) == 0 { 300 return nil, nil 301 } 302 303 // Prepare consent providers string 304 cpStr = fmt.Sprintf("[%s]", strings.Replace(cpStr, ".", ",", -1)) 305 cpMap[consentedProvidersKey] = json.RawMessage(cpStr) 306 307 cpJSON, err := json.Marshal(cpMap) 308 if err != nil { 309 return nil, err 310 } 311 userExtMap[consentProvidersSettingsOutKey] = cpJSON 312 313 extJson, err := json.Marshal(userExtMap) 314 if err != nil { 315 return nil, err 316 } 317 318 return extJson, nil 319 } 320 321 func getImpExtWithRewardedInventory(imp openrtb2.Imp) ([]byte, error) { 322 var ext = make(map[string]json.RawMessage) 323 if err := json.Unmarshal(imp.Ext, &ext); err != nil { 324 return nil, err 325 } 326 327 prebidJSONValue, prebidJSONFound := ext["prebid"] 328 if !prebidJSONFound { 329 return nil, nil 330 } 331 332 var prebidMap = make(map[string]json.RawMessage) 333 if err := json.Unmarshal(prebidJSONValue, &prebidMap); err != nil { 334 return nil, err 335 } 336 337 if rewardedInventory, foundRewardedInventory := prebidMap[isRewardedInventory]; foundRewardedInventory && string(rewardedInventory) == stateRewardedInventoryEnable { 338 ext[isRewardedInventory] = json.RawMessage(`true`) 339 impExt, err := json.Marshal(ext) 340 if err != nil { 341 return nil, err 342 } 343 344 return impExt, nil 345 } 346 347 return nil, nil 348 } 349 350 func (a *ImprovedigitalAdapter) buildEndpointURL(imp openrtb2.Imp) string { 351 publisherEndpoint := "" 352 var impBidder ImpExtBidder 353 354 err := json.Unmarshal(imp.Ext, &impBidder) 355 if err == nil && impBidder.Bidder.PublisherID != 0 { 356 publisherEndpoint = strconv.Itoa(impBidder.Bidder.PublisherID) + "/" 357 } 358 359 return strings.Replace(a.endpoint, publisherEndpointParam, publisherEndpoint, -1) 360 }