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