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

     1  package yandex
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strconv"
     9  	"strings"
    10  	"text/template"
    11  
    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/macros"
    17  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    18  )
    19  
    20  const (
    21  	refererQueryKey  = "target-ref"
    22  	currencyQueryKey = "ssp-cur"
    23  	impIdQueryKey    = "imp-id"
    24  )
    25  
    26  // Composite id of an ad placement
    27  type yandexPlacementID struct {
    28  	PageID string
    29  	ImpID  string
    30  }
    31  
    32  type adapter struct {
    33  	endpoint *template.Template
    34  }
    35  
    36  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    37  	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
    38  	if err != nil {
    39  		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
    40  	}
    41  
    42  	bidder := &adapter{
    43  		endpoint: template,
    44  	}
    45  
    46  	return bidder, nil
    47  }
    48  
    49  func (a *adapter) MakeRequests(requestData *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    50  	var (
    51  		requests []*adapters.RequestData
    52  		errors   []error
    53  	)
    54  
    55  	referer := getReferer(requestData)
    56  	currency := getCurrency(requestData)
    57  
    58  	for i := range requestData.Imp {
    59  		imp := requestData.Imp[i]
    60  
    61  		placementId, err := getYandexPlacementId(imp)
    62  		if err != nil {
    63  			errors = append(errors, err)
    64  			continue
    65  		}
    66  
    67  		if err := modifyImp(&imp); err != nil {
    68  			errors = append(errors, err)
    69  			continue
    70  		}
    71  
    72  		resolvedUrl, err := a.resolveUrl(*placementId, referer, currency)
    73  		if err != nil {
    74  			errors = append(errors, err)
    75  			continue
    76  		}
    77  
    78  		splittedRequestData := splitRequestDataByImp(requestData, imp)
    79  
    80  		requestBody, err := json.Marshal(splittedRequestData)
    81  		if err != nil {
    82  			errors = append(errors, err)
    83  			continue
    84  		}
    85  
    86  		requests = append(requests, &adapters.RequestData{
    87  			Method:  "POST",
    88  			Uri:     resolvedUrl,
    89  			Body:    requestBody,
    90  			Headers: getHeaders(&splittedRequestData),
    91  			ImpIDs:  openrtb_ext.GetImpIDs(splittedRequestData.Imp),
    92  		})
    93  	}
    94  
    95  	return requests, errors
    96  }
    97  
    98  func getHeaders(request *openrtb2.BidRequest) http.Header {
    99  	headers := http.Header{}
   100  
   101  	if request.Device != nil && request.Site != nil {
   102  		addNonEmptyHeaders(&headers, map[string]string{
   103  			"Referer":         request.Site.Page,
   104  			"Accept-Language": request.Device.Language,
   105  			"User-Agent":      request.Device.UA,
   106  			"X-Forwarded-For": request.Device.IP,
   107  			"X-Real-Ip":       request.Device.IP,
   108  			"Content-Type":    "application/json;charset=utf-8",
   109  			"Accept":          "application/json",
   110  		})
   111  	}
   112  
   113  	return headers
   114  }
   115  
   116  func addNonEmptyHeaders(headers *http.Header, headerValues map[string]string) {
   117  	for key, value := range headerValues {
   118  		if len(value) > 0 {
   119  			headers.Add(key, value)
   120  		}
   121  	}
   122  }
   123  
   124  // Request is in shared memory, so we have to make a shallow copy for further modification (imp is already a shallow copy)
   125  func splitRequestDataByImp(request *openrtb2.BidRequest, imp openrtb2.Imp) openrtb2.BidRequest {
   126  	requestCopy := *request
   127  	requestCopy.Imp = []openrtb2.Imp{imp}
   128  
   129  	return requestCopy
   130  }
   131  
   132  func getYandexPlacementId(imp openrtb2.Imp) (*yandexPlacementID, error) {
   133  	var ext adapters.ExtImpBidder
   134  	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
   135  		return nil, &errortypes.BadInput{
   136  			Message: fmt.Sprintf("imp %s: unable to unmarshal ext", imp.ID),
   137  		}
   138  	}
   139  
   140  	var yandexExt openrtb_ext.ExtImpYandex
   141  	if err := json.Unmarshal(ext.Bidder, &yandexExt); err != nil {
   142  		return nil, &errortypes.BadInput{
   143  			Message: fmt.Sprintf("imp %s: unable to unmarshal ext.bidder: %v", imp.ID, err),
   144  		}
   145  	}
   146  
   147  	placementID, err := mapExtToPlacementID(yandexExt)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	return placementID, nil
   153  }
   154  
   155  func mapExtToPlacementID(yandexExt openrtb_ext.ExtImpYandex) (*yandexPlacementID, error) {
   156  	var placementID yandexPlacementID
   157  
   158  	if len(yandexExt.PlacementID) == 0 {
   159  		placementID.ImpID = strconv.Itoa(int(yandexExt.ImpID))
   160  		placementID.PageID = strconv.Itoa(int(yandexExt.PageID))
   161  		return &placementID, nil
   162  	}
   163  
   164  	idParts := strings.Split(yandexExt.PlacementID, "-")
   165  
   166  	numericIdParts := []string{}
   167  
   168  	for _, idPart := range idParts {
   169  		if _, err := strconv.Atoi(idPart); err == nil {
   170  			numericIdParts = append(numericIdParts, idPart)
   171  		}
   172  	}
   173  
   174  	if len(numericIdParts) < 2 {
   175  		return nil, &errortypes.BadInput{
   176  			Message: fmt.Sprintf("invalid placement id, it must contain two parts: %s", yandexExt.PlacementID),
   177  		}
   178  	}
   179  
   180  	placementID.ImpID = numericIdParts[len(numericIdParts)-1]
   181  	placementID.PageID = numericIdParts[len(numericIdParts)-2]
   182  
   183  	return &placementID, nil
   184  }
   185  
   186  func modifyImp(imp *openrtb2.Imp) error {
   187  	if imp.Banner != nil {
   188  		banner, err := modifyBanner(*imp.Banner)
   189  		if banner != nil {
   190  			imp.Banner = banner
   191  		}
   192  		return err
   193  	}
   194  
   195  	if imp.Native != nil {
   196  		return nil
   197  	}
   198  
   199  	return &errortypes.BadInput{
   200  		Message: fmt.Sprintf("Unsupported format. Yandex only supports banner and native types. Ignoring imp id #%s", imp.ID),
   201  	}
   202  }
   203  
   204  func modifyBanner(banner openrtb2.Banner) (*openrtb2.Banner, error) {
   205  	format := banner.Format
   206  
   207  	if banner.W == nil || banner.H == nil || *banner.W == 0 || *banner.H == 0 {
   208  		if len(format) == 0 {
   209  			return nil, &errortypes.BadInput{
   210  				Message: "Invalid size provided for Banner",
   211  			}
   212  		}
   213  
   214  		firstFormat := format[0]
   215  		banner.H = &firstFormat.H
   216  		banner.W = &firstFormat.W
   217  	}
   218  
   219  	return &banner, nil
   220  }
   221  
   222  // "Un-templates" the endpoint by replacing macroses and adding the required query parameters
   223  func (a *adapter) resolveUrl(placementID yandexPlacementID, referer string, currency string) (string, error) {
   224  	params := macros.EndpointTemplateParams{PageID: placementID.PageID}
   225  
   226  	endpointStr, err := macros.ResolveMacros(a.endpoint, params)
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  
   231  	parsedUrl, err := url.Parse(endpointStr)
   232  	if err != nil {
   233  		return "", err
   234  	}
   235  
   236  	addNonEmptyQueryParams(parsedUrl, map[string]string{
   237  		refererQueryKey:  referer,
   238  		currencyQueryKey: currency,
   239  		impIdQueryKey:    placementID.ImpID,
   240  	})
   241  
   242  	return parsedUrl.String(), nil
   243  }
   244  
   245  func addNonEmptyQueryParams(url *url.URL, queryMap map[string]string) {
   246  	query := url.Query()
   247  	for key, value := range queryMap {
   248  		if len(value) > 0 {
   249  			query.Add(key, value)
   250  		}
   251  	}
   252  
   253  	url.RawQuery = query.Encode()
   254  }
   255  
   256  func getReferer(request *openrtb2.BidRequest) string {
   257  	if request.Site == nil {
   258  		return ""
   259  	}
   260  
   261  	return request.Site.Domain
   262  }
   263  
   264  func getCurrency(request *openrtb2.BidRequest) string {
   265  	if len(request.Cur) == 0 {
   266  		return ""
   267  	}
   268  
   269  	return request.Cur[0]
   270  }
   271  
   272  func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   273  
   274  	if adapters.IsResponseStatusCodeNoContent(responseData) {
   275  		return nil, nil
   276  	}
   277  
   278  	if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
   279  		return nil, []error{err}
   280  	}
   281  
   282  	var bidResponse openrtb2.BidResponse
   283  	if err := json.Unmarshal(responseData.Body, &bidResponse); err != nil {
   284  		return nil, []error{&errortypes.BadServerResponse{
   285  			Message: fmt.Sprintf("Bad server response: %d", err),
   286  		}}
   287  	}
   288  
   289  	bidResponseWithCapacity := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
   290  
   291  	var errors []error
   292  
   293  	impMap := map[string]*openrtb2.Imp{}
   294  	for i := range request.Imp {
   295  		imp := request.Imp[i]
   296  
   297  		impMap[imp.ID] = &imp
   298  	}
   299  
   300  	for _, seatBid := range bidResponse.SeatBid {
   301  		for i := range seatBid.Bid {
   302  			bid := seatBid.Bid[i]
   303  
   304  			imp, exists := impMap[bid.ImpID]
   305  			if !exists {
   306  				errors = append(errors, &errortypes.BadInput{
   307  					Message: fmt.Sprintf("Invalid bid imp ID #%s does not match any imp IDs from the original bid request", bid.ImpID),
   308  				})
   309  				continue
   310  			}
   311  
   312  			bidType, err := getBidType(*imp)
   313  			if err != nil {
   314  				errors = append(errors, err)
   315  				continue
   316  			}
   317  
   318  			bidResponseWithCapacity.Bids = append(bidResponseWithCapacity.Bids, &adapters.TypedBid{
   319  				Bid:     &bid,
   320  				BidType: bidType,
   321  			})
   322  		}
   323  	}
   324  
   325  	return bidResponseWithCapacity, errors
   326  }
   327  
   328  func getBidType(imp openrtb2.Imp) (openrtb_ext.BidType, error) {
   329  	if imp.Native != nil {
   330  		return openrtb_ext.BidTypeNative, nil
   331  	}
   332  
   333  	if imp.Banner != nil {
   334  		return openrtb_ext.BidTypeBanner, nil
   335  	}
   336  
   337  	return "", &errortypes.BadInput{
   338  		Message: fmt.Sprintf("Processing an invalid impression; cannot resolve impression type for imp #%s", imp.ID),
   339  	}
   340  }