github.com/prebid/prebid-server/v2@v2.18.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/v20/openrtb2" 14 "github.com/prebid/prebid-server/v2/adapters" 15 "github.com/prebid/prebid-server/v2/config" 16 "github.com/prebid/prebid-server/v2/errortypes" 17 "github.com/prebid/prebid-server/v2/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 ImpIDs: getImpIDs(formattedRequest.Imp), 132 } 133 134 return []*adapters.RequestData{requestData}, nil 135 } 136 137 func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, externalResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { 138 if externalResponse.StatusCode == http.StatusNoContent { 139 return nil, nil 140 } 141 142 if externalResponse.StatusCode != http.StatusOK { 143 err := &errortypes.BadServerResponse{ 144 Message: fmt.Sprintf("Unexpected status code: %d.", externalResponse.StatusCode), 145 } 146 return nil, []error{err} 147 } 148 149 var response openrtb2.BidResponse 150 if err := json.Unmarshal(externalResponse.Body, &response); err != nil { 151 return nil, []error{err} 152 } 153 154 bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp)) 155 bidResponse.Currency = response.Cur 156 157 var errors []error 158 for _, seatBid := range response.SeatBid { 159 for _, bid := range seatBid.Bid { 160 if err := a.impToBid(internalRequest, seatBid, bid, bidResponse); err != nil { 161 errors = append(errors, err) 162 } 163 } 164 } 165 166 return bidResponse, errors 167 } 168 169 func (a *adapter) impToBid(internalRequest *openrtb2.BidRequest, seatBid openrtb2.SeatBid, bid openrtb2.Bid, 170 bidResponse *adapters.BidderResponse) error { 171 var bidType openrtb_ext.BidType 172 173 /* 174 Determine bid type 175 At this moment we only check if bid contains Adm property 176 177 Later updates will check for video & native data 178 */ 179 if bid.AdM != "" { 180 bidType = openrtb_ext.BidTypeBanner 181 } 182 183 /* 184 Recover original ImpID 185 (stored on request in TagID) 186 */ 187 impID, err := getOriginalImpID(bid.ImpID, internalRequest.Imp) 188 if err != nil { 189 return err 190 } 191 bid.ImpID = impID 192 193 // read additional data from proxy 194 var bidDataExt responseExt 195 if err := json.Unmarshal(bid.Ext, &bidDataExt); err != nil { 196 return err 197 } 198 /* 199 use correct ad creation method for a detected bid type 200 right now, we are only creating banner ads 201 if type is not detected / supported, throw error 202 */ 203 if bidType != openrtb_ext.BidTypeBanner { 204 return errNotSupportedFormat 205 } 206 207 var adCreationError error 208 bid.AdM, adCreationError = a.createBannerAd(bid, bidDataExt, internalRequest, seatBid.Seat) 209 if adCreationError != nil { 210 return adCreationError 211 } 212 // append bid to responses 213 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 214 Bid: &bid, 215 BidType: bidType, 216 }) 217 218 return nil 219 } 220 221 func getOriginalImpID(impID string, imps []openrtb2.Imp) (string, error) { 222 for _, imp := range imps { 223 if imp.ID == impID { 224 return imp.TagID, nil 225 } 226 } 227 228 return "", errImpNotFound 229 } 230 231 func (a *adapter) createBannerAd(bid openrtb2.Bid, ext responseExt, request *openrtb2.BidRequest, seat string) (string, error) { 232 if strings.Contains(bid.AdM, "<!--preformatted-->") { 233 // Banner ad is already formatted 234 return bid.AdM, nil 235 } 236 237 // create McAd payload 238 var mcad = mcAd{ 239 Id: request.ID, 240 Seat: seat, 241 SeatBid: []openrtb2.SeatBid{ 242 {Bid: []openrtb2.Bid{bid}}, 243 }, 244 } 245 246 bannerData := &templatePayload{ 247 SiteId: ext.SiteId, 248 SlotId: ext.SlotId, 249 AdLabel: ext.AdLabel, 250 PubId: ext.PublisherId, 251 Page: request.Site.Page, 252 Referer: request.Site.Ref, 253 McAd: mcad, 254 Inver: prebidServerIntegrationType, 255 } 256 257 var filledTemplate bytes.Buffer 258 if err := a.bannerTemplate.Execute(&filledTemplate, bannerData); err != nil { 259 return "", err 260 } 261 262 return filledTemplate.String(), nil 263 } 264 265 func getImpSize(imp openrtb2.Imp) string { 266 if imp.Banner == nil || len(imp.Banner.Format) == 0 { 267 return impFallbackSize 268 } 269 270 var ( 271 areaMax int64 272 impSize = impFallbackSize 273 ) 274 275 for _, size := range imp.Banner.Format { 276 area := size.W * size.H 277 if area > areaMax { 278 areaMax = area 279 impSize = fmt.Sprintf("%dx%d", size.W, size.H) 280 } 281 } 282 283 return impSize 284 } 285 286 // getBidParameters reads additional data for this imp (site id , placement id, test) 287 // Errors in parameters do not break imp flow, and thus are not returned 288 func getBidParameters(imp openrtb2.Imp) openrtb_ext.ExtImpSspbc { 289 var extBidder adapters.ExtImpBidder 290 var extSSP openrtb_ext.ExtImpSspbc 291 292 if err := json.Unmarshal(imp.Ext, &extBidder); err == nil { 293 _ = json.Unmarshal(extBidder.Bidder, &extSSP) 294 } 295 296 return extSSP 297 } 298 299 // getRequestType checks what kind of request we have. It can either be: 300 // - a standard request, where all Imps have complete site / placement data 301 // - a oneCodeRequest, where site / placement data has to be determined by server 302 // - a test request, where server returns fixed example ads 303 func getRequestType(request *openrtb2.BidRequest) int { 304 incompleteImps := 0 305 306 for _, imp := range request.Imp { 307 // Read data for this imp 308 extSSP := getBidParameters(imp) 309 310 if extSSP.IsTest != 0 { 311 return requestTypeTest 312 } 313 314 if extSSP.SiteId == "" || extSSP.Id == "" { 315 incompleteImps += 1 316 } 317 } 318 319 if incompleteImps > 0 { 320 return requestTypeOneCode 321 } 322 323 return requestTypeStandard 324 } 325 326 func formatSspBcRequest(request *openrtb2.BidRequest) (*openrtb2.BidRequest, error) { 327 if request.Site == nil { 328 return nil, errSiteNill 329 } 330 331 var siteID string 332 333 // determine what kind of request we are dealing with 334 requestType := getRequestType(request) 335 336 for i, imp := range request.Imp { 337 // read ext data for the impression 338 extSSP := getBidParameters(imp) 339 340 // store SiteID 341 if extSSP.SiteId != "" { 342 siteID = extSSP.SiteId 343 } 344 345 // save current imp.id (adUnit name) as imp.tagid 346 // we will recover it in makeBids 347 imp.TagID = imp.ID 348 349 // if there is a placement id, and this is not a oneCodeRequest, use it in imp.id 350 if extSSP.Id != "" && requestType != requestTypeOneCode { 351 imp.ID = extSSP.Id 352 } 353 354 // check imp size and update e.ext - send pbslot, pbsize 355 // inability to set bid.ext will cause request to be invalid 356 impSize := getImpSize(imp) 357 impExt := requestImpExt{ 358 Data: adSlotData{ 359 PbSlot: imp.TagID, 360 PbSize: impSize, 361 }, 362 } 363 364 impExtJSON, err := json.Marshal(impExt) 365 if err != nil { 366 return nil, err 367 } 368 imp.Ext = impExtJSON 369 // save updated imp 370 request.Imp[i] = imp 371 } 372 373 siteCopy := *request.Site 374 request.Site = &siteCopy 375 376 /* 377 update site ID 378 for oneCode request it has to be blank 379 for other requests it should be equal to 380 SiteId from one of the bids 381 */ 382 if requestType == requestTypeOneCode || siteID == "" { 383 request.Site.ID = "" 384 } else { 385 request.Site.ID = siteID 386 } 387 388 // add domain info 389 if siteURL, err := url.Parse(request.Site.Page); err == nil { 390 request.Site.Domain = siteURL.Hostname() 391 } 392 393 // set TEST Flag 394 if requestType == requestTypeTest { 395 request.Test = 1 396 } 397 398 return request, nil 399 } 400 401 // getImpIDs uses imp.TagID instead of imp.ID as formattedRequest stores imp.ID in imp.TagID 402 func getImpIDs(imps []openrtb2.Imp) []string { 403 impIDs := make([]string, len(imps)) 404 for i := range imps { 405 impIDs[i] = imps[i].TagID 406 } 407 return impIDs 408 }