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  }