github.com/prebid/prebid-server@v0.275.0/adapters/consumable/consumable.go (about)

     1  package consumable
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/prebid/openrtb/v19/openrtb2"
    12  	"github.com/prebid/prebid-server/adapters"
    13  	"github.com/prebid/prebid-server/config"
    14  	"github.com/prebid/prebid-server/errortypes"
    15  	"github.com/prebid/prebid-server/openrtb_ext"
    16  	"github.com/prebid/prebid-server/privacy/ccpa"
    17  )
    18  
    19  type ConsumableAdapter struct {
    20  	clock    instant
    21  	endpoint string
    22  }
    23  
    24  type bidRequest struct {
    25  	Placements         []placement          `json:"placements"`
    26  	Time               int64                `json:"time"`
    27  	NetworkId          int                  `json:"networkId,omitempty"`
    28  	SiteId             int                  `json:"siteId"`
    29  	UnitId             int                  `json:"unitId"`
    30  	UnitName           string               `json:"unitName,omitempty"`
    31  	IncludePricingData bool                 `json:"includePricingData"`
    32  	User               user                 `json:"user,omitempty"`
    33  	Referrer           string               `json:"referrer,omitempty"`
    34  	Ip                 string               `json:"ip,omitempty"`
    35  	Url                string               `json:"url,omitempty"`
    36  	EnableBotFiltering bool                 `json:"enableBotFiltering,omitempty"`
    37  	Parallel           bool                 `json:"parallel"`
    38  	CCPA               string               `json:"ccpa,omitempty"`
    39  	GDPR               *bidGdpr             `json:"gdpr,omitempty"`
    40  	Coppa              bool                 `json:"coppa,omitempty"`
    41  	SChain             openrtb2.SupplyChain `json:"schain"`
    42  	Content            *openrtb2.Content    `json:"content,omitempty"`
    43  	GPP                string               `json:"gpp,omitempty"`
    44  	GPPSID             []int8               `json:"gpp_sid,omitempty"`
    45  }
    46  
    47  type placement struct {
    48  	DivName   string `json:"divName"`
    49  	NetworkId int    `json:"networkId,omitempty"`
    50  	SiteId    int    `json:"siteId"`
    51  	UnitId    int    `json:"unitId"`
    52  	UnitName  string `json:"unitName,omitempty"`
    53  	AdTypes   []int  `json:"adTypes"`
    54  }
    55  
    56  type user struct {
    57  	Key  string         `json:"key,omitempty"`
    58  	Eids []openrtb2.EID `json:"eids,omitempty"`
    59  }
    60  
    61  type bidGdpr struct {
    62  	Applies *bool  `json:"applies,omitempty"`
    63  	Consent string `json:"consent,omitempty"`
    64  }
    65  
    66  type bidResponse struct {
    67  	Decisions map[string]decision `json:"decisions"` // map by bidId
    68  }
    69  
    70  /**
    71   * See https://dev.adzerk.com/v1.0/reference/response
    72   */
    73  type decision struct {
    74  	Pricing       *pricing   `json:"pricing"`
    75  	AdID          int64      `json:"adId"`
    76  	BidderName    string     `json:"bidderName,omitempty"`
    77  	CreativeID    string     `json:"creativeId,omitempty"`
    78  	Contents      []contents `json:"contents"`
    79  	ImpressionUrl *string    `json:"impressionUrl,omitempty"`
    80  	Width         uint64     `json:"width,omitempty"`  // Consumable extension, not defined by Adzerk
    81  	Height        uint64     `json:"height,omitempty"` // Consumable extension, not defined by Adzerk
    82  	Adomain       []string   `json:"adomain,omitempty"`
    83  	Cats          []string   `json:"cats,omitempty"`
    84  }
    85  
    86  type contents struct {
    87  	Body string `json:"body"`
    88  }
    89  
    90  type pricing struct {
    91  	ClearPrice *float64 `json:"clearPrice"`
    92  }
    93  
    94  func (a *ConsumableAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    95  	var errs []error
    96  
    97  	headers := http.Header{
    98  		"Content-Type": {"application/json"},
    99  		"Accept":       {"application/json"},
   100  	}
   101  
   102  	if request.Device != nil {
   103  		if request.Device.UA != "" {
   104  			headers.Set("User-Agent", request.Device.UA)
   105  		}
   106  
   107  		if request.Device.IP != "" {
   108  			headers.Set("Forwarded", "for="+request.Device.IP)
   109  			headers.Set("X-Forwarded-For", request.Device.IP)
   110  		}
   111  	}
   112  
   113  	// Set azk cookie to one we got via sync
   114  	if request.User != nil {
   115  		userID := strings.TrimSpace(request.User.BuyerUID)
   116  		if len(userID) > 0 {
   117  			headers.Add("Cookie", fmt.Sprintf("%s=%s", "azk", userID))
   118  		}
   119  	}
   120  
   121  	if request.Site != nil && request.Site.Page != "" {
   122  		headers.Set("Referer", request.Site.Page)
   123  
   124  		pageUrl, err := url.Parse(request.Site.Page)
   125  		if err != nil {
   126  			errs = append(errs, err)
   127  		} else {
   128  			origin := url.URL{
   129  				Scheme: pageUrl.Scheme,
   130  				Opaque: pageUrl.Opaque,
   131  				Host:   pageUrl.Host,
   132  			}
   133  			headers.Set("Origin", origin.String())
   134  		}
   135  	}
   136  
   137  	body := bidRequest{
   138  		Placements:         make([]placement, len(request.Imp)),
   139  		Time:               a.clock.Now().Unix(),
   140  		IncludePricingData: true,
   141  		EnableBotFiltering: true,
   142  		Parallel:           true,
   143  	}
   144  
   145  	if request.Site != nil {
   146  		body.Referrer = request.Site.Ref // Effectively the previous page to the page where the ad will be shown
   147  		body.Url = request.Site.Page     // where the impression will be made
   148  	}
   149  
   150  	gdpr := bidGdpr{}
   151  
   152  	ccpaPolicy, err := ccpa.ReadFromRequest(request)
   153  	if err != nil {
   154  		errs = append(errs, err)
   155  	} else {
   156  		body.CCPA = ccpaPolicy.Consent
   157  	}
   158  
   159  	if request.Regs != nil && request.Regs.Ext != nil {
   160  		var extRegs openrtb_ext.ExtRegs
   161  		if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil {
   162  			errs = append(errs, err)
   163  		} else {
   164  			if extRegs.GDPR != nil {
   165  				applies := *extRegs.GDPR != 0
   166  				gdpr.Applies = &applies
   167  				body.GDPR = &gdpr
   168  			}
   169  		}
   170  	}
   171  
   172  	if request.User != nil && request.User.Ext != nil {
   173  		var extUser openrtb_ext.ExtUser
   174  		if err := json.Unmarshal(request.User.Ext, &extUser); err != nil {
   175  			errs = append(errs, err)
   176  		} else {
   177  			gdpr.Consent = extUser.Consent
   178  			body.GDPR = &gdpr
   179  
   180  			if hasEids(extUser.Eids) {
   181  				body.User.Eids = extUser.Eids
   182  			}
   183  		}
   184  	}
   185  
   186  	if request.Source != nil && request.Source.Ext != nil {
   187  		var extSChain openrtb_ext.ExtRequestPrebidSChain
   188  		if err := json.Unmarshal(request.Source.Ext, &extSChain); err != nil {
   189  			errs = append(errs, err)
   190  		} else {
   191  			body.SChain = extSChain.SChain
   192  		}
   193  	}
   194  
   195  	body.Coppa = request.Regs != nil && request.Regs.COPPA > 0
   196  
   197  	if request.Regs != nil && request.Regs.GPP != "" {
   198  		body.GPP = request.Regs.GPP
   199  	}
   200  
   201  	if request.Regs != nil && request.Regs.GPPSID != nil {
   202  		body.GPPSID = request.Regs.GPPSID
   203  	}
   204  
   205  	if request.Site != nil && request.Site.Content != nil {
   206  		body.Content = request.Site.Content
   207  	} else if request.App != nil && request.App.Content != nil {
   208  		body.Content = request.App.Content
   209  	}
   210  
   211  	for i, impression := range request.Imp {
   212  
   213  		_, consumableExt, err := extractExtensions(impression)
   214  
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  
   219  		// These get set on the first one in observed working requests
   220  		if i == 0 {
   221  			body.NetworkId = consumableExt.NetworkId
   222  			body.SiteId = consumableExt.SiteId
   223  			body.UnitId = consumableExt.UnitId
   224  			body.UnitName = consumableExt.UnitName
   225  		}
   226  
   227  		body.Placements[i] = placement{
   228  			DivName:   impression.ID,
   229  			NetworkId: consumableExt.NetworkId,
   230  			SiteId:    consumableExt.SiteId,
   231  			UnitId:    consumableExt.UnitId,
   232  			UnitName:  consumableExt.UnitName,
   233  			AdTypes:   getSizeCodes(impression.Banner.Format), // was adTypes: bid.adTypes || getSize(bid.sizes) in prebid.js
   234  		}
   235  	}
   236  
   237  	bodyBytes, err := json.Marshal(body)
   238  	if err != nil {
   239  		return nil, []error{err}
   240  	}
   241  
   242  	requests := []*adapters.RequestData{
   243  		{
   244  			Method:  "POST",
   245  			Uri:     "https://e.serverbid.com/api/v2",
   246  			Body:    bodyBytes,
   247  			Headers: headers,
   248  		},
   249  	}
   250  
   251  	return requests, errs
   252  }
   253  
   254  /*
   255  internal original request in OpenRTB, external = result of us having converted it (what comes out of MakeRequests)
   256  */
   257  func (a *ConsumableAdapter) MakeBids(
   258  	internalRequest *openrtb2.BidRequest,
   259  	externalRequest *adapters.RequestData,
   260  	response *adapters.ResponseData,
   261  ) (*adapters.BidderResponse, []error) {
   262  
   263  	if response.StatusCode == http.StatusNoContent {
   264  		return nil, nil
   265  	}
   266  
   267  	if response.StatusCode == http.StatusBadRequest {
   268  		return nil, []error{&errortypes.BadInput{
   269  			Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   270  		}}
   271  	}
   272  
   273  	if response.StatusCode != http.StatusOK {
   274  		return nil, []error{&errortypes.BadServerResponse{
   275  			Message: fmt.Sprintf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   276  		}}
   277  	}
   278  
   279  	var serverResponse bidResponse // response from Consumable
   280  	if err := json.Unmarshal(response.Body, &serverResponse); err != nil {
   281  		return nil, []error{&errortypes.BadServerResponse{
   282  			Message: fmt.Sprintf("error while decoding response, err: %s", err),
   283  		}}
   284  	}
   285  
   286  	bidderResponse := adapters.NewBidderResponse()
   287  	var errors []error
   288  
   289  	for impID, decision := range serverResponse.Decisions {
   290  
   291  		if decision.Pricing != nil && decision.Pricing.ClearPrice != nil {
   292  			bid := openrtb2.Bid{}
   293  			bid.ID = internalRequest.ID
   294  			bid.ImpID = impID
   295  			bid.Price = *decision.Pricing.ClearPrice
   296  			bid.AdM = retrieveAd(decision)
   297  			bid.W = int64(decision.Width)
   298  			bid.H = int64(decision.Height)
   299  			bid.CrID = strconv.FormatInt(decision.AdID, 10)
   300  			bid.Exp = 30 // TODO: Check this is intention of TTL
   301  			bid.ADomain = decision.Adomain
   302  			bid.Cat = decision.Cats
   303  			// not yet ported from prebid.js adapter
   304  			//bid.requestId = bidId;
   305  			//bid.currency = 'USD';
   306  			//bid.netRevenue = true;
   307  			//bid.referrer = utils.getTopWindowUrl();
   308  
   309  			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
   310  				Bid: &bid,
   311  				// Consumable units are always HTML, never VAST.
   312  				// From Prebid's point of view, this means that Consumable units
   313  				// are always "banners".
   314  				BidType: openrtb_ext.BidTypeBanner,
   315  			})
   316  		}
   317  	}
   318  	return bidderResponse, errors
   319  }
   320  
   321  func extractExtensions(impression openrtb2.Imp) (*adapters.ExtImpBidder, *openrtb_ext.ExtImpConsumable, []error) {
   322  	var bidderExt adapters.ExtImpBidder
   323  	if err := json.Unmarshal(impression.Ext, &bidderExt); err != nil {
   324  		return nil, nil, []error{&errortypes.BadInput{
   325  			Message: err.Error(),
   326  		}}
   327  	}
   328  
   329  	var consumableExt openrtb_ext.ExtImpConsumable
   330  	if err := json.Unmarshal(bidderExt.Bidder, &consumableExt); err != nil {
   331  		return nil, nil, []error{&errortypes.BadInput{
   332  			Message: err.Error(),
   333  		}}
   334  	}
   335  
   336  	return &bidderExt, &consumableExt, nil
   337  }
   338  
   339  // Builder builds a new instance of the Consumable adapter for the given bidder with the given config.
   340  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   341  	bidder := &ConsumableAdapter{
   342  		clock:    realInstant{},
   343  		endpoint: config.Endpoint,
   344  	}
   345  	return bidder, nil
   346  }
   347  
   348  func hasEids(eids []openrtb2.EID) bool {
   349  	for i := 0; i < len(eids); i++ {
   350  		if len(eids[i].UIDs) > 0 && eids[i].UIDs[0].ID != "" {
   351  			return true
   352  		}
   353  	}
   354  	return false
   355  }