github.com/prebid/prebid-server@v0.275.0/adapters/sspBC/sspbc.go (about) 1 package sspBC 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "html/template" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "github.com/prebid/openrtb/v19/openrtb2" 14 "github.com/prebid/prebid-server/adapters" 15 "github.com/prebid/prebid-server/config" 16 "github.com/prebid/prebid-server/errortypes" 17 "github.com/prebid/prebid-server/openrtb_ext" 18 ) 19 20 const ( 21 adapterVersion = "5.8" 22 impFallbackSize = "1x1" 23 requestTypeStandard = 1 24 requestTypeOneCode = 2 25 requestTypeTest = 3 26 prebidServerIntegrationType = "4" 27 ) 28 29 var ( 30 errSiteNill = errors.New("site cannot be nill") 31 errImpNotFound = errors.New("imp not found") 32 errNotSupportedFormat = errors.New("bid format is not supported") 33 ) 34 35 // mcAd defines the MC payload for banner ads. 36 type mcAd struct { 37 Id string `json:"id"` 38 Seat string `json:"seat"` 39 SeatBid []openrtb2.SeatBid `json:"seatbid"` 40 } 41 42 // adSlotData defines struct used for the oneCode detection. 43 type adSlotData struct { 44 PbSlot string `json:"pbslot"` 45 PbSize string `json:"pbsize"` 46 } 47 48 // templatePayload represents the banner template payload. 49 type templatePayload struct { 50 SiteId string `json:"siteid"` 51 SlotId string `json:"slotid"` 52 AdLabel string `json:"adlabel"` 53 PubId string `json:"pubid"` 54 Page string `json:"page"` 55 Referer string `json:"referer"` 56 McAd mcAd `json:"mcad"` 57 Inver string `json:"inver"` 58 } 59 60 // requestImpExt represents the ext field of the request imp field. 61 type requestImpExt struct { 62 Data adSlotData `json:"data"` 63 } 64 65 // responseExt represents ext data added by proxy. 66 type responseExt struct { 67 AdLabel string `json:"adlabel"` 68 PublisherId string `json:"pubid"` 69 SiteId string `json:"siteid"` 70 SlotId string `json:"slotid"` 71 } 72 73 type adapter struct { 74 endpoint string 75 bannerTemplate *template.Template 76 } 77 78 // ---------------ADAPTER INTERFACE------------------ 79 // Builder builds a new instance of the sspBC adapter 80 func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 81 // HTML template used to create banner ads 82 const bannerHTML = `<html><head><title></title><meta charset="UTF-8"><meta name="viewport" content="` + 83 `width=device-width, initial-scale=1.0"><style> body { background-color: transparent; margin: 0;` + 84 ` padding: 0; }</style><script> window.rekid = {{.SiteId}}; window.slot = {{.SlotId}}; window.ad` + 85 `label = '{{.AdLabel}}'; window.pubid = '{{.PubId}}'; window.wp_sn = 'sspbc_go'; window.page = '` + 86 `{{.Page}}'; window.ref = '{{.Referer}}'; window.mcad = {{.McAd}}; window.in` + 87 `ver = '{{.Inver}}'; </script></head><body><div id="c"></div><script async c` + 88 `rossorigin nomodule src="//std.wpcdn.pl/wpjslib/wpjslib-inline.js" id="wpjslib"></script><scrip` + 89 `t async crossorigin type="module" src="//std.wpcdn.pl/wpjslib6/wpjslib-inline.js" id="wpjslib6"` + 90 `></script></body></html>` 91 92 bannerTemplate, err := template.New("banner").Parse(bannerHTML) 93 if err != nil { 94 return nil, err 95 } 96 97 bidder := &adapter{ 98 endpoint: config.Endpoint, 99 bannerTemplate: bannerTemplate, 100 } 101 102 return bidder, nil 103 } 104 105 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 106 formattedRequest, err := formatSspBcRequest(request) 107 if err != nil { 108 return nil, []error{err} 109 } 110 111 requestJSON, err := json.Marshal(formattedRequest) 112 if err != nil { 113 return nil, []error{err} 114 } 115 116 requestURL, err := url.Parse(a.endpoint) 117 if err != nil { 118 return nil, []error{err} 119 } 120 121 // add query parameters to request 122 queryParams := requestURL.Query() 123 queryParams.Add("bdver", adapterVersion) 124 queryParams.Add("inver", prebidServerIntegrationType) 125 requestURL.RawQuery = queryParams.Encode() 126 127 requestData := &adapters.RequestData{ 128 Method: http.MethodPost, 129 Uri: requestURL.String(), 130 Body: requestJSON, 131 } 132 133 return []*adapters.RequestData{requestData}, nil 134 } 135 136 func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, externalResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { 137 if externalResponse.StatusCode == http.StatusNoContent { 138 return nil, nil 139 } 140 141 if externalResponse.StatusCode != http.StatusOK { 142 err := &errortypes.BadServerResponse{ 143 Message: fmt.Sprintf("Unexpected status code: %d.", externalResponse.StatusCode), 144 } 145 return nil, []error{err} 146 } 147 148 var response openrtb2.BidResponse 149 if err := json.Unmarshal(externalResponse.Body, &response); err != nil { 150 return nil, []error{err} 151 } 152 153 bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) 154 bidResponse.Currency = response.Cur 155 156 var errors []error 157 for _, seatBid := range response.SeatBid { 158 for _, bid := range seatBid.Bid { 159 if err := a.impToBid(internalRequest, seatBid, bid, bidResponse); err != nil { 160 errors = append(errors, err) 161 } 162 } 163 } 164 165 return bidResponse, errors 166 } 167 168 func (a *adapter) impToBid(internalRequest *openrtb2.BidRequest, seatBid openrtb2.SeatBid, bid openrtb2.Bid, 169 bidResponse *adapters.BidderResponse) error { 170 var bidType openrtb_ext.BidType 171 172 /* 173 Determine bid type 174 At this moment we only check if bid contains Adm property 175 176 Later updates will check for video & native data 177 */ 178 if bid.AdM != "" { 179 bidType = openrtb_ext.BidTypeBanner 180 } 181 182 /* 183 Recover original ImpID 184 (stored on request in TagID) 185 */ 186 impID, err := getOriginalImpID(bid.ImpID, internalRequest.Imp) 187 if err != nil { 188 return err 189 } 190 bid.ImpID = impID 191 192 // read additional data from proxy 193 var bidDataExt responseExt 194 if err := json.Unmarshal(bid.Ext, &bidDataExt); err != nil { 195 return err 196 } 197 /* 198 use correct ad creation method for a detected bid type 199 right now, we are only creating banner ads 200 if type is not detected / supported, throw error 201 */ 202 if bidType != openrtb_ext.BidTypeBanner { 203 return errNotSupportedFormat 204 } 205 206 var adCreationError error 207 bid.AdM, adCreationError = a.createBannerAd(bid, bidDataExt, internalRequest, seatBid.Seat) 208 if adCreationError != nil { 209 return adCreationError 210 } 211 // append bid to responses 212 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 213 Bid: &bid, 214 BidType: bidType, 215 }) 216 217 return nil 218 } 219 220 func getOriginalImpID(impID string, imps []openrtb2.Imp) (string, error) { 221 for _, imp := range imps { 222 if imp.ID == impID { 223 return imp.TagID, nil 224 } 225 } 226 227 return "", errImpNotFound 228 } 229 230 func (a *adapter) createBannerAd(bid openrtb2.Bid, ext responseExt, request *openrtb2.BidRequest, seat string) (string, error) { 231 if strings.Contains(bid.AdM, "<!--preformatted-->") { 232 // Banner ad is already formatted 233 return bid.AdM, nil 234 } 235 236 // create McAd payload 237 var mcad = mcAd{ 238 Id: request.ID, 239 Seat: seat, 240 SeatBid: []openrtb2.SeatBid{ 241 {Bid: []openrtb2.Bid{bid}}, 242 }, 243 } 244 245 bannerData := &templatePayload{ 246 SiteId: ext.SiteId, 247 SlotId: ext.SlotId, 248 AdLabel: ext.AdLabel, 249 PubId: ext.PublisherId, 250 Page: request.Site.Page, 251 Referer: request.Site.Ref, 252 McAd: mcad, 253 Inver: prebidServerIntegrationType, 254 } 255 256 var filledTemplate bytes.Buffer 257 if err := a.bannerTemplate.Execute(&filledTemplate, bannerData); err != nil { 258 return "", err 259 } 260 261 return filledTemplate.String(), nil 262 } 263 264 func getImpSize(imp openrtb2.Imp) string { 265 if imp.Banner == nil || len(imp.Banner.Format) == 0 { 266 return impFallbackSize 267 } 268 269 var ( 270 areaMax int64 271 impSize = impFallbackSize 272 ) 273 274 for _, size := range imp.Banner.Format { 275 area := size.W * size.H 276 if area > areaMax { 277 areaMax = area 278 impSize = fmt.Sprintf("%dx%d", size.W, size.H) 279 } 280 } 281 282 return impSize 283 } 284 285 // getBidParameters reads additional data for this imp (site id , placement id, test) 286 // Errors in parameters do not break imp flow, and thus are not returned 287 func getBidParameters(imp openrtb2.Imp) openrtb_ext.ExtImpSspbc { 288 var extBidder adapters.ExtImpBidder 289 var extSSP openrtb_ext.ExtImpSspbc 290 291 if err := json.Unmarshal(imp.Ext, &extBidder); err == nil { 292 _ = json.Unmarshal(extBidder.Bidder, &extSSP) 293 } 294 295 return extSSP 296 } 297 298 // getRequestType checks what kind of request we have. It can either be: 299 // - a standard request, where all Imps have complete site / placement data 300 // - a oneCodeRequest, where site / placement data has to be determined by server 301 // - a test request, where server returns fixed example ads 302 func getRequestType(request *openrtb2.BidRequest) int { 303 incompleteImps := 0 304 305 for _, imp := range request.Imp { 306 // Read data for this imp 307 extSSP := getBidParameters(imp) 308 309 if extSSP.IsTest != 0 { 310 return requestTypeTest 311 } 312 313 if extSSP.SiteId == "" || extSSP.Id == "" { 314 incompleteImps += 1 315 } 316 } 317 318 if incompleteImps > 0 { 319 return requestTypeOneCode 320 } 321 322 return requestTypeStandard 323 } 324 325 func formatSspBcRequest(request *openrtb2.BidRequest) (*openrtb2.BidRequest, error) { 326 if request.Site == nil { 327 return nil, errSiteNill 328 } 329 330 var siteID string 331 332 // determine what kind of request we are dealing with 333 requestType := getRequestType(request) 334 335 for i, imp := range request.Imp { 336 // read ext data for the impression 337 extSSP := getBidParameters(imp) 338 339 // store SiteID 340 if extSSP.SiteId != "" { 341 siteID = extSSP.SiteId 342 } 343 344 // save current imp.id (adUnit name) as imp.tagid 345 // we will recover it in makeBids 346 imp.TagID = imp.ID 347 348 // if there is a placement id, and this is not a oneCodeRequest, use it in imp.id 349 if extSSP.Id != "" && requestType != requestTypeOneCode { 350 imp.ID = extSSP.Id 351 } 352 353 // check imp size and update e.ext - send pbslot, pbsize 354 // inability to set bid.ext will cause request to be invalid 355 impSize := getImpSize(imp) 356 impExt := requestImpExt{ 357 Data: adSlotData{ 358 PbSlot: imp.TagID, 359 PbSize: impSize, 360 }, 361 } 362 363 impExtJSON, err := json.Marshal(impExt) 364 if err != nil { 365 return nil, err 366 } 367 imp.Ext = impExtJSON 368 // save updated imp 369 request.Imp[i] = imp 370 } 371 372 siteCopy := *request.Site 373 request.Site = &siteCopy 374 375 /* 376 update site ID 377 for oneCode request it has to be blank 378 for other requests it should be equal to 379 SiteId from one of the bids 380 */ 381 if requestType == requestTypeOneCode || siteID == "" { 382 request.Site.ID = "" 383 } else { 384 request.Site.ID = siteID 385 } 386 387 // add domain info 388 if siteURL, err := url.Parse(request.Site.Page); err == nil { 389 request.Site.Domain = siteURL.Hostname() 390 } 391 392 // set TEST Flag 393 if requestType == requestTypeTest { 394 request.Test = 1 395 } 396 397 return request, nil 398 }