github.com/prebid/prebid-server@v0.275.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/v19/adcom1" 15 "github.com/prebid/openrtb/v19/openrtb2" 16 "github.com/prebid/prebid-server/adapters" 17 "github.com/prebid/prebid-server/config" 18 "github.com/prebid/prebid-server/errortypes" 19 "github.com/prebid/prebid-server/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 } 208 209 requests := []*adapters.RequestData{&requestData} 210 211 return requests, errors 212 } 213 214 func isMobileDevice(request *openrtb2.BidRequest) bool { 215 return request.Device != nil && (request.Device.DeviceType == adcom1.DeviceMobile || request.Device.DeviceType == adcom1.DevicePhone || request.Device.DeviceType == adcom1.DeviceTablet) 216 } 217 218 func getImpTypeRequest(request *openrtb2.BidRequest, totalImps int) int { 219 220 impType := impTypeBanner 221 for i := 0; i < totalImps; i++ { 222 imp := request.Imp[i] 223 if imp.Video != nil { 224 if imp.Video.Placement == vastInstream { 225 impType = vastInstream 226 } else if impType == impTypeBanner { 227 impType = vastOutstream 228 } 229 } 230 } 231 232 return impType 233 234 } 235 func cleanName(name string) string { 236 for _, step := range cleanNameSteps { 237 name = step.expression.ReplaceAllString(name, step.replacementString) 238 } 239 return name 240 } 241 242 func verifyImp(imp *openrtb2.Imp, isMobile bool, impType int) (*openrtb_ext.ExtImpEPlanning, error) { 243 var bidderExt adapters.ExtImpBidder 244 245 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 246 return nil, &errortypes.BadInput{ 247 Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), 248 } 249 } 250 251 if impType > impTypeBanner { 252 if impType == vastInstream { 253 // In-stream 254 if imp.Video == nil || imp.Video.Placement != vastInstream { 255 return nil, &errortypes.BadInput{ 256 Message: fmt.Sprintf("Ignoring imp id=%s, auction instream and imp no instream", imp.ID), 257 } 258 } 259 } else { 260 //Out-stream 261 if imp.Video == nil || imp.Video.Placement == vastInstream { 262 return nil, &errortypes.BadInput{ 263 Message: fmt.Sprintf("Ignoring imp id=%s, auction outstream and imp no outstream", imp.ID), 264 } 265 } 266 } 267 } 268 269 impExt := openrtb_ext.ExtImpEPlanning{} 270 err := json.Unmarshal(bidderExt.Bidder, &impExt) 271 if err != nil { 272 return nil, &errortypes.BadInput{ 273 Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), 274 } 275 } 276 277 if impExt.ClientID == "" { 278 return nil, &errortypes.BadInput{ 279 Message: fmt.Sprintf("Ignoring imp id=%s, no ClientID present", imp.ID), 280 } 281 } 282 283 width, height := getSizeFromImp(imp, isMobile) 284 285 if width == 0 && height == 0 { 286 if imp.Video != nil { 287 impExt.SizeString = vastDefaultSize 288 } else { 289 impExt.SizeString = nullSize 290 } 291 } else { 292 impExt.SizeString = fmt.Sprintf("%dx%d", width, height) 293 } 294 295 if impExt.AdUnitCode == "" { 296 impExt.AdUnitCode = impExt.SizeString 297 } 298 299 return &impExt, nil 300 } 301 302 func searchSizePriority(hashedFormats map[string]int, format []openrtb2.Format, priorityOrderForSizesAsc []string) (int64, int64) { 303 for i := len(priorityOrderForSizesAsc) - 1; i >= 0; i-- { 304 if formatIndex, wasFound := hashedFormats[priorityOrderForSizesAsc[i]]; wasFound { 305 return format[formatIndex].W, format[formatIndex].H 306 } 307 } 308 return format[0].W, format[0].H 309 } 310 311 func getSizeFromImp(imp *openrtb2.Imp, isMobile bool) (int64, int64) { 312 313 if imp.Video != nil && imp.Video.W > 0 && imp.Video.H > 0 { 314 return imp.Video.W, imp.Video.H 315 } 316 317 if imp.Banner != nil { 318 if imp.Banner.W != nil && imp.Banner.H != nil { 319 return *imp.Banner.W, *imp.Banner.H 320 } 321 322 if imp.Banner.Format != nil { 323 hashedFormats := make(map[string]int, len(imp.Banner.Format)) 324 325 for i, format := range imp.Banner.Format { 326 if format.W != 0 && format.H != 0 { 327 hashedFormats[fmt.Sprintf("%dx%d", format.W, format.H)] = i 328 } 329 } 330 331 if isMobile { 332 return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForMobileSizesAsc) 333 } else { 334 return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForDesktopSizesAsc) 335 } 336 } 337 } 338 339 return 0, 0 340 } 341 342 func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) { 343 if len(headerValue) > 0 { 344 headers.Add(headerName, headerValue) 345 } 346 } 347 348 func (adapter *EPlanningAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 349 if response.StatusCode == http.StatusNoContent { 350 return nil, nil 351 } 352 353 if response.StatusCode == http.StatusBadRequest { 354 return nil, []error{&errortypes.BadInput{ 355 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 356 }} 357 } 358 359 if response.StatusCode != http.StatusOK { 360 return nil, []error{&errortypes.BadServerResponse{ 361 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 362 }} 363 } 364 365 var parsedResponse hbResponse 366 if err := json.Unmarshal(response.Body, &parsedResponse); err != nil { 367 return nil, []error{&errortypes.BadServerResponse{ 368 Message: fmt.Sprintf("Error unmarshaling HB response: %s", err.Error()), 369 }} 370 } 371 372 isMobile := isMobileDevice(internalRequest) 373 impType := getImpTypeRequest(internalRequest, len(internalRequest.Imp)) 374 375 bidResponse := adapters.NewBidderResponse() 376 377 spaceNameToImpID := make(map[string]string) 378 379 index_vast := 0 380 for _, imp := range internalRequest.Imp { 381 extImp, err := verifyImp(&imp, isMobile, impType) 382 if err != nil { 383 continue 384 } 385 386 name := cleanName(extImp.AdUnitCode) 387 if imp.Video != nil { 388 name = getNameVideo(extImp.SizeString, index_vast) 389 index_vast++ 390 } 391 spaceNameToImpID[name] = imp.ID 392 } 393 394 for _, space := range parsedResponse.Spaces { 395 for _, ad := range space.Ads { 396 if price, err := strconv.ParseFloat(ad.Price, 64); err == nil { 397 bid := openrtb2.Bid{ 398 ID: ad.ImpressionID, 399 AdID: ad.AdID, 400 ImpID: spaceNameToImpID[space.Name], 401 Price: price, 402 AdM: ad.AdM, 403 CrID: ad.CrID, 404 W: int64(ad.Width), 405 H: int64(ad.Height), 406 } 407 408 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 409 Bid: &bid, 410 BidType: getBidType(impType), 411 }) 412 } 413 } 414 } 415 416 return bidResponse, nil 417 } 418 419 func getBidType(impType int) openrtb_ext.BidType { 420 421 bidType := openrtb_ext.BidTypeBanner 422 if impType > 0 { 423 bidType = openrtb_ext.BidTypeVideo 424 } 425 return bidType 426 } 427 428 func getNameVideo(size string, index_vast int) string { 429 return "video_" + size + "_" + strconv.Itoa(index_vast) 430 } 431 432 // Builder builds a new instance of the EPlanning adapter for the given bidder with the given config. 433 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 434 bidder := &EPlanningAdapter{ 435 URI: config.Endpoint, 436 testing: false, 437 } 438 return bidder, nil 439 }