github.com/prebid/prebid-server/v2@v2.18.0/adapters/adnuntius/adnuntius.go (about)

     1  package adnuntius
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/buger/jsonparser"
    12  	"github.com/prebid/openrtb/v20/openrtb2"
    13  	"github.com/prebid/prebid-server/v2/adapters"
    14  	"github.com/prebid/prebid-server/v2/config"
    15  	"github.com/prebid/prebid-server/v2/errortypes"
    16  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    17  	"github.com/prebid/prebid-server/v2/util/timeutil"
    18  )
    19  
    20  type QueryString map[string]string
    21  type adapter struct {
    22  	time      timeutil.Time
    23  	endpoint  string
    24  	extraInfo string
    25  }
    26  type adnAdunit struct {
    27  	AuId       string    `json:"auId"`
    28  	TargetId   string    `json:"targetId"`
    29  	Dimensions [][]int64 `json:"dimensions,omitempty"`
    30  	MaxDeals   int       `json:"maxDeals,omitempty"`
    31  }
    32  
    33  type extDeviceAdnuntius struct {
    34  	NoCookies bool `json:"noCookies,omitempty"`
    35  }
    36  type siteExt struct {
    37  	Data interface{} `json:"data"`
    38  }
    39  
    40  type Ad struct {
    41  	Bid struct {
    42  		Amount   float64
    43  		Currency string
    44  	}
    45  	NetBid struct {
    46  		Amount float64
    47  	}
    48  	GrossBid struct {
    49  		Amount float64
    50  	}
    51  	DealID          string `json:"dealId,omitempty"`
    52  	AdId            string
    53  	CreativeWidth   string
    54  	CreativeHeight  string
    55  	CreativeId      string
    56  	LineItemId      string
    57  	Html            string
    58  	DestinationUrls map[string]string
    59  }
    60  
    61  type AdUnit struct {
    62  	AuId       string
    63  	TargetId   string
    64  	Html       string
    65  	ResponseId string
    66  	Ads        []Ad
    67  	Deals      []Ad `json:"deals,omitempty"`
    68  }
    69  
    70  type AdnResponse struct {
    71  	AdUnits []AdUnit
    72  }
    73  type adnMetaData struct {
    74  	Usi string `json:"usi,omitempty"`
    75  }
    76  type adnRequest struct {
    77  	AdUnits   []adnAdunit `json:"adUnits"`
    78  	MetaData  adnMetaData `json:"metaData,omitempty"`
    79  	Context   string      `json:"context,omitempty"`
    80  	KeyValues interface{} `json:"kv,omitempty"`
    81  }
    82  
    83  type RequestExt struct {
    84  	Bidder adnAdunit `json:"bidder"`
    85  }
    86  
    87  const defaultNetwork = "default"
    88  const defaultSite = "unknown"
    89  const minutesInHour = 60
    90  
    91  // Builder builds a new instance of the Adnuntius adapter for the given bidder with the given config.
    92  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    93  	bidder := &adapter{
    94  		time:      &timeutil.RealTime{},
    95  		endpoint:  config.Endpoint,
    96  		extraInfo: config.ExtraAdapterInfo,
    97  	}
    98  
    99  	return bidder, nil
   100  }
   101  
   102  func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
   103  	return a.generateRequests(*request)
   104  }
   105  
   106  func setHeaders(ortbRequest openrtb2.BidRequest) http.Header {
   107  
   108  	headers := http.Header{}
   109  	headers.Add("Content-Type", "application/json;charset=utf-8")
   110  	headers.Add("Accept", "application/json")
   111  	if ortbRequest.Device != nil {
   112  		if ortbRequest.Device.IP != "" {
   113  			headers.Add("X-Forwarded-For", ortbRequest.Device.IP)
   114  		}
   115  		if ortbRequest.Device.UA != "" {
   116  			headers.Add("user-agent", ortbRequest.Device.UA)
   117  		}
   118  	}
   119  	return headers
   120  }
   121  
   122  func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter, noCookies bool) (string, []error) {
   123  	uri, err := url.Parse(a.endpoint)
   124  	endpointUrl := a.endpoint
   125  	if err != nil {
   126  		return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)}
   127  	}
   128  
   129  	gdpr, consent, err := getGDPR(&ortbRequest)
   130  	if err != nil {
   131  		return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)}
   132  	}
   133  
   134  	if !noCookies {
   135  		var deviceExt extDeviceAdnuntius
   136  		if ortbRequest.Device != nil && ortbRequest.Device.Ext != nil {
   137  			if err := json.Unmarshal(ortbRequest.Device.Ext, &deviceExt); err != nil {
   138  				return "", []error{fmt.Errorf("failed to parse Adnuntius endpoint: %v", err)}
   139  			}
   140  		}
   141  
   142  		if deviceExt.NoCookies {
   143  			noCookies = true
   144  		}
   145  	}
   146  
   147  	_, offset := a.time.Now().Zone()
   148  	tzo := -offset / minutesInHour
   149  
   150  	q := uri.Query()
   151  	if gdpr != "" {
   152  		endpointUrl = a.extraInfo
   153  		q.Set("gdpr", gdpr)
   154  	}
   155  
   156  	if consent != "" {
   157  		q.Set("consentString", consent)
   158  	}
   159  
   160  	if noCookies {
   161  		q.Set("noCookies", "true")
   162  	}
   163  
   164  	q.Set("tzo", fmt.Sprint(tzo))
   165  	q.Set("format", "json")
   166  
   167  	url := endpointUrl + "?" + q.Encode()
   168  	return url, nil
   169  }
   170  
   171  func getImpSizes(imp openrtb2.Imp) [][]int64 {
   172  
   173  	if len(imp.Banner.Format) > 0 {
   174  		sizes := make([][]int64, len(imp.Banner.Format))
   175  		for i, format := range imp.Banner.Format {
   176  			sizes[i] = []int64{format.W, format.H}
   177  		}
   178  
   179  		return sizes
   180  	}
   181  
   182  	if imp.Banner.W != nil && imp.Banner.H != nil {
   183  		size := make([][]int64, 1)
   184  		size[0] = []int64{*imp.Banner.W, *imp.Banner.H}
   185  		return size
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  /*
   192  Generate the requests to Adnuntius to reduce the amount of requests going out.
   193  */
   194  func (a *adapter) generateRequests(ortbRequest openrtb2.BidRequest) ([]*adapters.RequestData, []error) {
   195  	var requestData []*adapters.RequestData
   196  	networkAdunitMap := make(map[string][]adnAdunit)
   197  	headers := setHeaders(ortbRequest)
   198  	var noCookies bool = false
   199  
   200  	for _, imp := range ortbRequest.Imp {
   201  		if imp.Banner == nil {
   202  			return nil, []error{&errortypes.BadInput{
   203  				Message: fmt.Sprintf("ignoring imp id=%s, Adnuntius supports only Banner", imp.ID),
   204  			}}
   205  		}
   206  
   207  		var bidderExt adapters.ExtImpBidder
   208  		if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   209  			return nil, []error{&errortypes.BadInput{
   210  				Message: fmt.Sprintf("Error unmarshalling ExtImpBidder: %s", err.Error()),
   211  			}}
   212  		}
   213  
   214  		var adnuntiusExt openrtb_ext.ImpExtAdnunitus
   215  		if err := json.Unmarshal(bidderExt.Bidder, &adnuntiusExt); err != nil {
   216  			return nil, []error{&errortypes.BadInput{
   217  				Message: fmt.Sprintf("Error unmarshalling ExtImpValues: %s", err.Error()),
   218  			}}
   219  		}
   220  
   221  		if adnuntiusExt.NoCookies {
   222  			noCookies = true
   223  		}
   224  
   225  		network := defaultNetwork
   226  		if adnuntiusExt.Network != "" {
   227  			network = adnuntiusExt.Network
   228  		}
   229  
   230  		adUnit := adnAdunit{
   231  			AuId:       adnuntiusExt.Auid,
   232  			TargetId:   fmt.Sprintf("%s-%s", adnuntiusExt.Auid, imp.ID),
   233  			Dimensions: getImpSizes(imp),
   234  		}
   235  		if adnuntiusExt.MaxDeals > 0 {
   236  			adUnit.MaxDeals = adnuntiusExt.MaxDeals
   237  		}
   238  		networkAdunitMap[network] = append(
   239  			networkAdunitMap[network],
   240  			adUnit)
   241  	}
   242  
   243  	endpoint, err := makeEndpointUrl(ortbRequest, a, noCookies)
   244  	if err != nil {
   245  		return nil, []error{&errortypes.BadInput{
   246  			Message: fmt.Sprintf("failed to parse URL: %s", err),
   247  		}}
   248  	}
   249  
   250  	site := defaultSite
   251  	if ortbRequest.Site != nil && ortbRequest.Site.Page != "" {
   252  		site = ortbRequest.Site.Page
   253  	}
   254  
   255  	extSite, erro := getSiteExtAsKv(&ortbRequest)
   256  	if erro != nil {
   257  		return nil, []error{fmt.Errorf("failed to parse site Ext: %v", err)}
   258  	}
   259  
   260  	for _, networkAdunits := range networkAdunitMap {
   261  
   262  		adnuntiusRequest := adnRequest{
   263  			AdUnits:   networkAdunits,
   264  			Context:   site,
   265  			KeyValues: extSite.Data,
   266  		}
   267  
   268  		var extUser openrtb_ext.ExtUser
   269  		if ortbRequest.User != nil && ortbRequest.User.Ext != nil {
   270  			if err := json.Unmarshal(ortbRequest.User.Ext, &extUser); err != nil {
   271  				return nil, []error{fmt.Errorf("failed to parse Ext User: %v", err)}
   272  			}
   273  		}
   274  
   275  		// Will change when our adserver can accept multiple user IDS
   276  		if extUser.Eids != nil && len(extUser.Eids) > 0 {
   277  			if len(extUser.Eids[0].UIDs) > 0 {
   278  				adnuntiusRequest.MetaData.Usi = extUser.Eids[0].UIDs[0].ID
   279  			}
   280  		}
   281  
   282  		ortbUser := ortbRequest.User
   283  		if ortbUser != nil {
   284  			ortbUserId := ortbRequest.User.ID
   285  			if ortbUserId != "" {
   286  				adnuntiusRequest.MetaData.Usi = ortbRequest.User.ID
   287  			}
   288  		}
   289  
   290  		adnJson, err := json.Marshal(adnuntiusRequest)
   291  		if err != nil {
   292  			return nil, []error{&errortypes.BadInput{
   293  				Message: fmt.Sprintf("Error unmarshalling adnuntius request: %s", err.Error()),
   294  			}}
   295  		}
   296  
   297  		requestData = append(requestData, &adapters.RequestData{
   298  			Method:  http.MethodPost,
   299  			Uri:     endpoint,
   300  			Body:    adnJson,
   301  			Headers: headers,
   302  			ImpIDs:  openrtb_ext.GetImpIDs(ortbRequest.Imp),
   303  		})
   304  
   305  	}
   306  
   307  	return requestData, nil
   308  }
   309  
   310  func (a *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   311  
   312  	if response.StatusCode == http.StatusBadRequest {
   313  		return nil, []error{&errortypes.BadInput{
   314  			Message: fmt.Sprintf("Status code: %d, Request malformed", response.StatusCode),
   315  		}}
   316  	}
   317  
   318  	if response.StatusCode != http.StatusOK {
   319  		return nil, []error{&errortypes.BadServerResponse{
   320  			Message: fmt.Sprintf("Status code: %d, Something went wrong with your request", response.StatusCode),
   321  		}}
   322  	}
   323  
   324  	var adnResponse AdnResponse
   325  	if err := json.Unmarshal(response.Body, &adnResponse); err != nil {
   326  		return nil, []error{err}
   327  	}
   328  
   329  	bidResponse, bidErr := generateBidResponse(&adnResponse, request)
   330  	if bidErr != nil {
   331  		return nil, bidErr
   332  	}
   333  
   334  	return bidResponse, nil
   335  }
   336  
   337  func getSiteExtAsKv(request *openrtb2.BidRequest) (siteExt, error) {
   338  	var extSite siteExt
   339  	if request.Site != nil && request.Site.Ext != nil {
   340  		if err := json.Unmarshal(request.Site.Ext, &extSite); err != nil {
   341  			return extSite, fmt.Errorf("failed to parse ExtSite in Adnuntius: %v", err)
   342  		}
   343  	}
   344  	return extSite, nil
   345  }
   346  
   347  func getGDPR(request *openrtb2.BidRequest) (string, string, error) {
   348  
   349  	gdpr := ""
   350  	var extRegs openrtb_ext.ExtRegs
   351  	if request.Regs != nil && request.Regs.Ext != nil {
   352  		if err := json.Unmarshal(request.Regs.Ext, &extRegs); err != nil {
   353  			return "", "", fmt.Errorf("failed to parse ExtRegs in Adnuntius GDPR check: %v", err)
   354  		}
   355  		if extRegs.GDPR != nil && (*extRegs.GDPR == 0 || *extRegs.GDPR == 1) {
   356  			gdpr = strconv.Itoa(int(*extRegs.GDPR))
   357  		}
   358  	}
   359  
   360  	consent := ""
   361  	if request.User != nil && request.User.Ext != nil {
   362  		var extUser openrtb_ext.ExtUser
   363  		if err := json.Unmarshal(request.User.Ext, &extUser); err != nil {
   364  			return "", "", fmt.Errorf("failed to parse ExtUser in Adnuntius GDPR check: %v", err)
   365  		}
   366  		consent = extUser.Consent
   367  	}
   368  
   369  	return gdpr, consent, nil
   370  }
   371  
   372  func generateAdResponse(ad Ad, imp openrtb2.Imp, html string, request *openrtb2.BidRequest) (*openrtb2.Bid, []error) {
   373  
   374  	creativeWidth, widthErr := strconv.ParseInt(ad.CreativeWidth, 10, 64)
   375  	if widthErr != nil {
   376  		return nil, []error{&errortypes.BadInput{
   377  			Message: fmt.Sprintf("Value of width: %s is not a string", ad.CreativeWidth),
   378  		}}
   379  	}
   380  
   381  	creativeHeight, heightErr := strconv.ParseInt(ad.CreativeHeight, 10, 64)
   382  	if heightErr != nil {
   383  		return nil, []error{&errortypes.BadInput{
   384  			Message: fmt.Sprintf("Value of height: %s is not a string", ad.CreativeHeight),
   385  		}}
   386  	}
   387  
   388  	price := ad.Bid.Amount
   389  
   390  	var bidderExt adapters.ExtImpBidder
   391  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   392  		return nil, []error{&errortypes.BadInput{
   393  			Message: fmt.Sprintf("Error unmarshalling ExtImpBidder: %s", err.Error()),
   394  		}}
   395  	}
   396  
   397  	var adnuntiusExt openrtb_ext.ImpExtAdnunitus
   398  	if err := json.Unmarshal(bidderExt.Bidder, &adnuntiusExt); err != nil {
   399  		return nil, []error{&errortypes.BadInput{
   400  			Message: fmt.Sprintf("Error unmarshalling ExtImpValues: %s", err.Error()),
   401  		}}
   402  	}
   403  
   404  	if adnuntiusExt.BidType != "" {
   405  		if strings.EqualFold(string(adnuntiusExt.BidType), "net") {
   406  			price = ad.NetBid.Amount
   407  		}
   408  		if strings.EqualFold(string(adnuntiusExt.BidType), "gross") {
   409  			price = ad.GrossBid.Amount
   410  		}
   411  	}
   412  
   413  	adDomain := []string{}
   414  	for _, url := range ad.DestinationUrls {
   415  		domainArray := strings.Split(url, "/")
   416  		domain := strings.Replace(domainArray[2], "www.", "", -1)
   417  		adDomain = append(adDomain, domain)
   418  	}
   419  
   420  	bid := openrtb2.Bid{
   421  		ID:      ad.AdId,
   422  		ImpID:   imp.ID,
   423  		W:       creativeWidth,
   424  		H:       creativeHeight,
   425  		AdID:    ad.AdId,
   426  		DealID:  ad.DealID,
   427  		CID:     ad.LineItemId,
   428  		CrID:    ad.CreativeId,
   429  		Price:   price * 1000,
   430  		AdM:     html,
   431  		ADomain: adDomain,
   432  	}
   433  	return &bid, nil
   434  
   435  }
   436  
   437  func generateBidResponse(adnResponse *AdnResponse, request *openrtb2.BidRequest) (*adapters.BidderResponse, []error) {
   438  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(adnResponse.AdUnits))
   439  	var currency string
   440  	adunitMap := map[string]AdUnit{}
   441  
   442  	for _, adnRespAdunit := range adnResponse.AdUnits {
   443  		adunitMap[adnRespAdunit.TargetId] = adnRespAdunit
   444  	}
   445  
   446  	for _, imp := range request.Imp {
   447  
   448  		auId, _, _, err := jsonparser.Get(imp.Ext, "bidder", "auId")
   449  		if err != nil {
   450  			return nil, []error{&errortypes.BadInput{
   451  				Message: fmt.Sprintf("Error at Bidder auId: %s", err.Error()),
   452  			}}
   453  		}
   454  
   455  		targetID := fmt.Sprintf("%s-%s", string(auId), imp.ID)
   456  		adunit := adunitMap[targetID]
   457  
   458  		if len(adunit.Ads) > 0 {
   459  
   460  			ad := adunit.Ads[0]
   461  			currency = ad.Bid.Currency
   462  
   463  			adBid, err := generateAdResponse(ad, imp, adunit.Html, request)
   464  			if err != nil {
   465  				return nil, []error{&errortypes.BadInput{
   466  					Message: "Error at ad generation",
   467  				}}
   468  			}
   469  
   470  			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   471  				Bid:     adBid,
   472  				BidType: "banner",
   473  			})
   474  
   475  			for _, deal := range adunit.Deals {
   476  				dealBid, err := generateAdResponse(deal, imp, deal.Html, request)
   477  				if err != nil {
   478  					return nil, []error{&errortypes.BadInput{
   479  						Message: "Error at ad generation",
   480  					}}
   481  				}
   482  
   483  				bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   484  					Bid:     dealBid,
   485  					BidType: "banner",
   486  				})
   487  			}
   488  
   489  		}
   490  
   491  	}
   492  	bidResponse.Currency = currency
   493  	return bidResponse, nil
   494  }