github.com/prebid/prebid-server/v2@v2.18.0/adapters/eplanning/eplanning.go (about) 1 package eplanning 2 3 import ( 4 "encoding/json" 5 "math/rand" 6 "net/http" 7 "net/url" 8 "strings" 9 10 "regexp" 11 12 "fmt" 13 14 "github.com/prebid/openrtb/v20/adcom1" 15 "github.com/prebid/openrtb/v20/openrtb2" 16 "github.com/prebid/prebid-server/v2/adapters" 17 "github.com/prebid/prebid-server/v2/config" 18 "github.com/prebid/prebid-server/v2/errortypes" 19 "github.com/prebid/prebid-server/v2/openrtb_ext" 20 21 "strconv" 22 ) 23 24 const nullSize = "1x1" 25 const defaultPageURL = "FILE" 26 const sec = "ROS" 27 const dfpClientID = "1" 28 const requestTargetInventory = "1" 29 const vastInstream = 1 30 const vastOutstream = 2 31 const vastVersionDefault = "3" 32 const vastDefaultSize = "640x480" 33 const impTypeBanner = 0 34 35 var priorityOrderForMobileSizesAsc = []string{"1x1", "300x50", "320x50", "300x250"} 36 var priorityOrderForDesktopSizesAsc = []string{"1x1", "970x90", "970x250", "160x600", "300x600", "728x90", "300x250"} 37 38 var cleanNameSteps = []cleanNameStep{ 39 {regexp.MustCompile(`_|\.|-|\/`), ""}, 40 {regexp.MustCompile(`\)\(|\(|\)|:`), "_"}, 41 {regexp.MustCompile(`^_+|_+$`), ""}, 42 } 43 44 type cleanNameStep struct { 45 expression *regexp.Regexp 46 replacementString string 47 } 48 49 type EPlanningAdapter struct { 50 URI string 51 testing bool 52 } 53 54 type hbResponse struct { 55 Spaces []hbResponseSpace `json:"sp"` 56 } 57 58 type hbResponseSpace struct { 59 Name string `json:"k"` 60 Ads []hbResponseAd `json:"a"` 61 } 62 63 type hbResponseAd struct { 64 ImpressionID string `json:"i"` 65 AdID string `json:"id,omitempty"` 66 Price string `json:"pr"` 67 AdM string `json:"adm"` 68 CrID string `json:"crid"` 69 Width uint64 `json:"w,omitempty"` 70 Height uint64 `json:"h,omitempty"` 71 } 72 73 func (adapter *EPlanningAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 74 errors := make([]error, 0, len(request.Imp)) 75 totalImps := len(request.Imp) 76 spacesStrings := make([]string, 0, totalImps) 77 totalRequests := 0 78 clientID := "" 79 isMobile := isMobileDevice(request) 80 impType := getImpTypeRequest(request, totalImps) 81 index_vast := 0 82 83 for i := 0; i < totalImps; i++ { 84 imp := request.Imp[i] 85 extImp, err := verifyImp(&imp, isMobile, impType) 86 if err != nil { 87 errors = append(errors, err) 88 continue 89 } 90 91 if clientID == "" { 92 clientID = extImp.ClientID 93 } 94 95 totalRequests++ 96 // Save valid imp 97 name := cleanName(extImp.AdUnitCode) 98 if imp.Video != nil { 99 name = getNameVideo(extImp.SizeString, index_vast) 100 spacesStrings = append(spacesStrings, name+":"+extImp.SizeString+";1") 101 index_vast++ 102 } else { 103 spacesStrings = append(spacesStrings, name+":"+extImp.SizeString) 104 } 105 106 } 107 108 if totalRequests == 0 { 109 return nil, errors 110 } 111 112 headers := http.Header{} 113 headers.Add("Content-Type", "application/json") 114 headers.Add("Accept", "application/json") 115 ip := "" 116 if request.Device != nil { 117 ip = request.Device.IP 118 addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA) 119 addHeaderIfNonEmpty(headers, "X-Forwarded-For", ip) 120 addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language) 121 if request.Device.DNT != nil { 122 addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT))) 123 } 124 } 125 126 pageURL := defaultPageURL 127 if request.Site != nil && request.Site.Page != "" { 128 pageURL = request.Site.Page 129 } 130 131 pageDomain := defaultPageURL 132 if request.Site != nil { 133 if request.Site.Domain != "" { 134 pageDomain = request.Site.Domain 135 } else if request.Site.Page != "" { 136 u, err := url.Parse(request.Site.Page) 137 if err != nil { 138 errors = append(errors, err) 139 return nil, errors 140 } 141 pageDomain = u.Hostname() 142 } 143 } 144 145 requestTarget := pageDomain 146 if request.App != nil && request.App.Bundle != "" { 147 requestTarget = request.App.Bundle 148 } 149 150 uriObj, err := url.Parse(adapter.URI) 151 if err != nil { 152 errors = append(errors, err) 153 return nil, errors 154 } 155 156 uriObj.Path = uriObj.Path + fmt.Sprintf("/%s/%s/%s/%s", clientID, dfpClientID, requestTarget, sec) 157 query := url.Values{} 158 query.Set("ncb", "1") 159 if request.App == nil { 160 query.Set("ur", pageURL) 161 } 162 query.Set("e", strings.Join(spacesStrings, "+")) 163 164 if request.User != nil && request.User.BuyerUID != "" { 165 query.Set("uid", request.User.BuyerUID) 166 } 167 168 if ip != "" { 169 query.Set("ip", ip) 170 } 171 172 var body []byte 173 if adapter.testing { 174 body = []byte("{}") 175 } else { 176 t := strconv.Itoa(rand.Int()) 177 query.Set("rnd", t) 178 body = nil 179 } 180 181 if request.App != nil { 182 if request.App.Name != "" { 183 query.Set("appn", request.App.Name) 184 } 185 if request.App.ID != "" { 186 query.Set("appid", request.App.ID) 187 } 188 if request.Device != nil && request.Device.IFA != "" { 189 query.Set("ifa", request.Device.IFA) 190 } 191 query.Set("app", requestTargetInventory) 192 } 193 194 if impType > 0 { 195 query.Set("vctx", strconv.Itoa(impType)) 196 query.Set("vv", vastVersionDefault) 197 } 198 199 uriObj.RawQuery = query.Encode() 200 uri := uriObj.String() 201 202 requestData := adapters.RequestData{ 203 Method: "GET", 204 Uri: uri, 205 Body: body, 206 Headers: headers, 207 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 208 } 209 210 requests := []*adapters.RequestData{&requestData} 211 212 return requests, errors 213 } 214 215 func isMobileDevice(request *openrtb2.BidRequest) bool { 216 return request.Device != nil && (request.Device.DeviceType == adcom1.DeviceMobile || request.Device.DeviceType == adcom1.DevicePhone || request.Device.DeviceType == adcom1.DeviceTablet) 217 } 218 219 func getImpTypeRequest(request *openrtb2.BidRequest, totalImps int) int { 220 221 impType := impTypeBanner 222 for i := 0; i < totalImps; i++ { 223 imp := request.Imp[i] 224 if imp.Video != nil { 225 if imp.Video.Placement == vastInstream { 226 impType = vastInstream 227 } else if impType == impTypeBanner { 228 impType = vastOutstream 229 } 230 } 231 } 232 233 return impType 234 235 } 236 func cleanName(name string) string { 237 for _, step := range cleanNameSteps { 238 name = step.expression.ReplaceAllString(name, step.replacementString) 239 } 240 return name 241 } 242 243 func verifyImp(imp *openrtb2.Imp, isMobile bool, impType int) (*openrtb_ext.ExtImpEPlanning, error) { 244 var bidderExt adapters.ExtImpBidder 245 246 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 247 return nil, &errortypes.BadInput{ 248 Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), 249 } 250 } 251 252 if impType > impTypeBanner { 253 if impType == vastInstream { 254 // In-stream 255 if imp.Video == nil || imp.Video.Placement != vastInstream { 256 return nil, &errortypes.BadInput{ 257 Message: fmt.Sprintf("Ignoring imp id=%s, auction instream and imp no instream", imp.ID), 258 } 259 } 260 } else { 261 //Out-stream 262 if imp.Video == nil || imp.Video.Placement == vastInstream { 263 return nil, &errortypes.BadInput{ 264 Message: fmt.Sprintf("Ignoring imp id=%s, auction outstream and imp no outstream", imp.ID), 265 } 266 } 267 } 268 } 269 270 impExt := openrtb_ext.ExtImpEPlanning{} 271 err := json.Unmarshal(bidderExt.Bidder, &impExt) 272 if err != nil { 273 return nil, &errortypes.BadInput{ 274 Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), 275 } 276 } 277 278 if impExt.ClientID == "" { 279 return nil, &errortypes.BadInput{ 280 Message: fmt.Sprintf("Ignoring imp id=%s, no ClientID present", imp.ID), 281 } 282 } 283 284 width, height := getSizeFromImp(imp, isMobile) 285 286 if width == 0 && height == 0 { 287 if imp.Video != nil { 288 impExt.SizeString = vastDefaultSize 289 } else { 290 impExt.SizeString = nullSize 291 } 292 } else { 293 impExt.SizeString = fmt.Sprintf("%dx%d", width, height) 294 } 295 296 if impExt.AdUnitCode == "" { 297 impExt.AdUnitCode = impExt.SizeString 298 } 299 300 return &impExt, nil 301 } 302 303 func searchSizePriority(hashedFormats map[string]int, format []openrtb2.Format, priorityOrderForSizesAsc []string) (int64, int64) { 304 for i := len(priorityOrderForSizesAsc) - 1; i >= 0; i-- { 305 if formatIndex, wasFound := hashedFormats[priorityOrderForSizesAsc[i]]; wasFound { 306 return format[formatIndex].W, format[formatIndex].H 307 } 308 } 309 return format[0].W, format[0].H 310 } 311 312 func getSizeFromImp(imp *openrtb2.Imp, isMobile bool) (int64, int64) { 313 314 if imp.Video != nil && imp.Video.W != nil && *imp.Video.W > 0 && imp.Video.H != nil && *imp.Video.H > 0 { 315 return *imp.Video.W, *imp.Video.H 316 } 317 318 if imp.Banner != nil { 319 if imp.Banner.W != nil && imp.Banner.H != nil { 320 return *imp.Banner.W, *imp.Banner.H 321 } 322 323 if imp.Banner.Format != nil { 324 hashedFormats := make(map[string]int, len(imp.Banner.Format)) 325 326 for i, format := range imp.Banner.Format { 327 if format.W != 0 && format.H != 0 { 328 hashedFormats[fmt.Sprintf("%dx%d", format.W, format.H)] = i 329 } 330 } 331 332 if isMobile { 333 return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForMobileSizesAsc) 334 } else { 335 return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForDesktopSizesAsc) 336 } 337 } 338 } 339 340 return 0, 0 341 } 342 343 func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) { 344 if len(headerValue) > 0 { 345 headers.Add(headerName, headerValue) 346 } 347 } 348 349 func (adapter *EPlanningAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 350 if response.StatusCode == http.StatusNoContent { 351 return nil, nil 352 } 353 354 if response.StatusCode == http.StatusBadRequest { 355 return nil, []error{&errortypes.BadInput{ 356 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 357 }} 358 } 359 360 if response.StatusCode != http.StatusOK { 361 return nil, []error{&errortypes.BadServerResponse{ 362 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 363 }} 364 } 365 366 var parsedResponse hbResponse 367 if err := json.Unmarshal(response.Body, &parsedResponse); err != nil { 368 return nil, []error{&errortypes.BadServerResponse{ 369 Message: fmt.Sprintf("Error unmarshaling HB response: %s", err.Error()), 370 }} 371 } 372 373 isMobile := isMobileDevice(internalRequest) 374 impType := getImpTypeRequest(internalRequest, len(internalRequest.Imp)) 375 376 bidResponse := adapters.NewBidderResponse() 377 378 spaceNameToImpID := make(map[string]string) 379 380 index_vast := 0 381 for _, imp := range internalRequest.Imp { 382 extImp, err := verifyImp(&imp, isMobile, impType) 383 if err != nil { 384 continue 385 } 386 387 name := cleanName(extImp.AdUnitCode) 388 if imp.Video != nil { 389 name = getNameVideo(extImp.SizeString, index_vast) 390 index_vast++ 391 } 392 spaceNameToImpID[name] = imp.ID 393 } 394 395 for _, space := range parsedResponse.Spaces { 396 for _, ad := range space.Ads { 397 if price, err := strconv.ParseFloat(ad.Price, 64); err == nil { 398 bid := openrtb2.Bid{ 399 ID: ad.ImpressionID, 400 AdID: ad.AdID, 401 ImpID: spaceNameToImpID[space.Name], 402 Price: price, 403 AdM: ad.AdM, 404 CrID: ad.CrID, 405 W: int64(ad.Width), 406 H: int64(ad.Height), 407 } 408 409 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 410 Bid: &bid, 411 BidType: getBidType(impType), 412 }) 413 } 414 } 415 } 416 417 return bidResponse, nil 418 } 419 420 func getBidType(impType int) openrtb_ext.BidType { 421 422 bidType := openrtb_ext.BidTypeBanner 423 if impType > 0 { 424 bidType = openrtb_ext.BidTypeVideo 425 } 426 return bidType 427 } 428 429 func getNameVideo(size string, index_vast int) string { 430 return "video_" + size + "_" + strconv.Itoa(index_vast) 431 } 432 433 // Builder builds a new instance of the EPlanning adapter for the given bidder with the given config. 434 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 435 bidder := &EPlanningAdapter{ 436 URI: config.Endpoint, 437 testing: false, 438 } 439 return bidder, nil 440 }