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

     1  package eplanning
     2  
     3  import (
     4  	"encoding/json"
     5  	"math/rand"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  
    10  	"regexp"
    11  
    12  	"fmt"
    13  
    14  	"github.com/prebid/openrtb/v20/adcom1"
    15  	"github.com/prebid/openrtb/v20/openrtb2"
    16  	"github.com/prebid/prebid-server/v2/adapters"
    17  	"github.com/prebid/prebid-server/v2/config"
    18  	"github.com/prebid/prebid-server/v2/errortypes"
    19  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    20  
    21  	"strconv"
    22  )
    23  
    24  const nullSize = "1x1"
    25  const defaultPageURL = "FILE"
    26  const sec = "ROS"
    27  const dfpClientID = "1"
    28  const requestTargetInventory = "1"
    29  const vastInstream = 1
    30  const vastOutstream = 2
    31  const vastVersionDefault = "3"
    32  const vastDefaultSize = "640x480"
    33  const impTypeBanner = 0
    34  
    35  var priorityOrderForMobileSizesAsc = []string{"1x1", "300x50", "320x50", "300x250"}
    36  var priorityOrderForDesktopSizesAsc = []string{"1x1", "970x90", "970x250", "160x600", "300x600", "728x90", "300x250"}
    37  
    38  var cleanNameSteps = []cleanNameStep{
    39  	{regexp.MustCompile(`_|\.|-|\/`), ""},
    40  	{regexp.MustCompile(`\)\(|\(|\)|:`), "_"},
    41  	{regexp.MustCompile(`^_+|_+$`), ""},
    42  }
    43  
    44  type cleanNameStep struct {
    45  	expression        *regexp.Regexp
    46  	replacementString string
    47  }
    48  
    49  type EPlanningAdapter struct {
    50  	URI     string
    51  	testing bool
    52  }
    53  
    54  type hbResponse struct {
    55  	Spaces []hbResponseSpace `json:"sp"`
    56  }
    57  
    58  type hbResponseSpace struct {
    59  	Name string         `json:"k"`
    60  	Ads  []hbResponseAd `json:"a"`
    61  }
    62  
    63  type hbResponseAd struct {
    64  	ImpressionID string `json:"i"`
    65  	AdID         string `json:"id,omitempty"`
    66  	Price        string `json:"pr"`
    67  	AdM          string `json:"adm"`
    68  	CrID         string `json:"crid"`
    69  	Width        uint64 `json:"w,omitempty"`
    70  	Height       uint64 `json:"h,omitempty"`
    71  }
    72  
    73  func (adapter *EPlanningAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    74  	errors := make([]error, 0, len(request.Imp))
    75  	totalImps := len(request.Imp)
    76  	spacesStrings := make([]string, 0, totalImps)
    77  	totalRequests := 0
    78  	clientID := ""
    79  	isMobile := isMobileDevice(request)
    80  	impType := getImpTypeRequest(request, totalImps)
    81  	index_vast := 0
    82  
    83  	for i := 0; i < totalImps; i++ {
    84  		imp := request.Imp[i]
    85  		extImp, err := verifyImp(&imp, isMobile, impType)
    86  		if err != nil {
    87  			errors = append(errors, err)
    88  			continue
    89  		}
    90  
    91  		if clientID == "" {
    92  			clientID = extImp.ClientID
    93  		}
    94  
    95  		totalRequests++
    96  		// Save valid imp
    97  		name := cleanName(extImp.AdUnitCode)
    98  		if imp.Video != nil {
    99  			name = getNameVideo(extImp.SizeString, index_vast)
   100  			spacesStrings = append(spacesStrings, name+":"+extImp.SizeString+";1")
   101  			index_vast++
   102  		} else {
   103  			spacesStrings = append(spacesStrings, name+":"+extImp.SizeString)
   104  		}
   105  
   106  	}
   107  
   108  	if totalRequests == 0 {
   109  		return nil, errors
   110  	}
   111  
   112  	headers := http.Header{}
   113  	headers.Add("Content-Type", "application/json")
   114  	headers.Add("Accept", "application/json")
   115  	ip := ""
   116  	if request.Device != nil {
   117  		ip = request.Device.IP
   118  		addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA)
   119  		addHeaderIfNonEmpty(headers, "X-Forwarded-For", ip)
   120  		addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language)
   121  		if request.Device.DNT != nil {
   122  			addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT)))
   123  		}
   124  	}
   125  
   126  	pageURL := defaultPageURL
   127  	if request.Site != nil && request.Site.Page != "" {
   128  		pageURL = request.Site.Page
   129  	}
   130  
   131  	pageDomain := defaultPageURL
   132  	if request.Site != nil {
   133  		if request.Site.Domain != "" {
   134  			pageDomain = request.Site.Domain
   135  		} else if request.Site.Page != "" {
   136  			u, err := url.Parse(request.Site.Page)
   137  			if err != nil {
   138  				errors = append(errors, err)
   139  				return nil, errors
   140  			}
   141  			pageDomain = u.Hostname()
   142  		}
   143  	}
   144  
   145  	requestTarget := pageDomain
   146  	if request.App != nil && request.App.Bundle != "" {
   147  		requestTarget = request.App.Bundle
   148  	}
   149  
   150  	uriObj, err := url.Parse(adapter.URI)
   151  	if err != nil {
   152  		errors = append(errors, err)
   153  		return nil, errors
   154  	}
   155  
   156  	uriObj.Path = uriObj.Path + fmt.Sprintf("/%s/%s/%s/%s", clientID, dfpClientID, requestTarget, sec)
   157  	query := url.Values{}
   158  	query.Set("ncb", "1")
   159  	if request.App == nil {
   160  		query.Set("ur", pageURL)
   161  	}
   162  	query.Set("e", strings.Join(spacesStrings, "+"))
   163  
   164  	if request.User != nil && request.User.BuyerUID != "" {
   165  		query.Set("uid", request.User.BuyerUID)
   166  	}
   167  
   168  	if ip != "" {
   169  		query.Set("ip", ip)
   170  	}
   171  
   172  	var body []byte
   173  	if adapter.testing {
   174  		body = []byte("{}")
   175  	} else {
   176  		t := strconv.Itoa(rand.Int())
   177  		query.Set("rnd", t)
   178  		body = nil
   179  	}
   180  
   181  	if request.App != nil {
   182  		if request.App.Name != "" {
   183  			query.Set("appn", request.App.Name)
   184  		}
   185  		if request.App.ID != "" {
   186  			query.Set("appid", request.App.ID)
   187  		}
   188  		if request.Device != nil && request.Device.IFA != "" {
   189  			query.Set("ifa", request.Device.IFA)
   190  		}
   191  		query.Set("app", requestTargetInventory)
   192  	}
   193  
   194  	if impType > 0 {
   195  		query.Set("vctx", strconv.Itoa(impType))
   196  		query.Set("vv", vastVersionDefault)
   197  	}
   198  
   199  	uriObj.RawQuery = query.Encode()
   200  	uri := uriObj.String()
   201  
   202  	requestData := adapters.RequestData{
   203  		Method:  "GET",
   204  		Uri:     uri,
   205  		Body:    body,
   206  		Headers: headers,
   207  		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   208  	}
   209  
   210  	requests := []*adapters.RequestData{&requestData}
   211  
   212  	return requests, errors
   213  }
   214  
   215  func isMobileDevice(request *openrtb2.BidRequest) bool {
   216  	return request.Device != nil && (request.Device.DeviceType == adcom1.DeviceMobile || request.Device.DeviceType == adcom1.DevicePhone || request.Device.DeviceType == adcom1.DeviceTablet)
   217  }
   218  
   219  func getImpTypeRequest(request *openrtb2.BidRequest, totalImps int) int {
   220  
   221  	impType := impTypeBanner
   222  	for i := 0; i < totalImps; i++ {
   223  		imp := request.Imp[i]
   224  		if imp.Video != nil {
   225  			if imp.Video.Placement == vastInstream {
   226  				impType = vastInstream
   227  			} else if impType == impTypeBanner {
   228  				impType = vastOutstream
   229  			}
   230  		}
   231  	}
   232  
   233  	return impType
   234  
   235  }
   236  func cleanName(name string) string {
   237  	for _, step := range cleanNameSteps {
   238  		name = step.expression.ReplaceAllString(name, step.replacementString)
   239  	}
   240  	return name
   241  }
   242  
   243  func verifyImp(imp *openrtb2.Imp, isMobile bool, impType int) (*openrtb_ext.ExtImpEPlanning, error) {
   244  	var bidderExt adapters.ExtImpBidder
   245  
   246  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   247  		return nil, &errortypes.BadInput{
   248  			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
   249  		}
   250  	}
   251  
   252  	if impType > impTypeBanner {
   253  		if impType == vastInstream {
   254  			// In-stream
   255  			if imp.Video == nil || imp.Video.Placement != vastInstream {
   256  				return nil, &errortypes.BadInput{
   257  					Message: fmt.Sprintf("Ignoring imp id=%s, auction instream and imp no instream", imp.ID),
   258  				}
   259  			}
   260  		} else {
   261  			//Out-stream
   262  			if imp.Video == nil || imp.Video.Placement == vastInstream {
   263  				return nil, &errortypes.BadInput{
   264  					Message: fmt.Sprintf("Ignoring imp id=%s, auction outstream and imp no outstream", imp.ID),
   265  				}
   266  			}
   267  		}
   268  	}
   269  
   270  	impExt := openrtb_ext.ExtImpEPlanning{}
   271  	err := json.Unmarshal(bidderExt.Bidder, &impExt)
   272  	if err != nil {
   273  		return nil, &errortypes.BadInput{
   274  			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
   275  		}
   276  	}
   277  
   278  	if impExt.ClientID == "" {
   279  		return nil, &errortypes.BadInput{
   280  			Message: fmt.Sprintf("Ignoring imp id=%s, no ClientID present", imp.ID),
   281  		}
   282  	}
   283  
   284  	width, height := getSizeFromImp(imp, isMobile)
   285  
   286  	if width == 0 && height == 0 {
   287  		if imp.Video != nil {
   288  			impExt.SizeString = vastDefaultSize
   289  		} else {
   290  			impExt.SizeString = nullSize
   291  		}
   292  	} else {
   293  		impExt.SizeString = fmt.Sprintf("%dx%d", width, height)
   294  	}
   295  
   296  	if impExt.AdUnitCode == "" {
   297  		impExt.AdUnitCode = impExt.SizeString
   298  	}
   299  
   300  	return &impExt, nil
   301  }
   302  
   303  func searchSizePriority(hashedFormats map[string]int, format []openrtb2.Format, priorityOrderForSizesAsc []string) (int64, int64) {
   304  	for i := len(priorityOrderForSizesAsc) - 1; i >= 0; i-- {
   305  		if formatIndex, wasFound := hashedFormats[priorityOrderForSizesAsc[i]]; wasFound {
   306  			return format[formatIndex].W, format[formatIndex].H
   307  		}
   308  	}
   309  	return format[0].W, format[0].H
   310  }
   311  
   312  func getSizeFromImp(imp *openrtb2.Imp, isMobile bool) (int64, int64) {
   313  
   314  	if imp.Video != nil && imp.Video.W != nil && *imp.Video.W > 0 && imp.Video.H != nil && *imp.Video.H > 0 {
   315  		return *imp.Video.W, *imp.Video.H
   316  	}
   317  
   318  	if imp.Banner != nil {
   319  		if imp.Banner.W != nil && imp.Banner.H != nil {
   320  			return *imp.Banner.W, *imp.Banner.H
   321  		}
   322  
   323  		if imp.Banner.Format != nil {
   324  			hashedFormats := make(map[string]int, len(imp.Banner.Format))
   325  
   326  			for i, format := range imp.Banner.Format {
   327  				if format.W != 0 && format.H != 0 {
   328  					hashedFormats[fmt.Sprintf("%dx%d", format.W, format.H)] = i
   329  				}
   330  			}
   331  
   332  			if isMobile {
   333  				return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForMobileSizesAsc)
   334  			} else {
   335  				return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForDesktopSizesAsc)
   336  			}
   337  		}
   338  	}
   339  
   340  	return 0, 0
   341  }
   342  
   343  func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) {
   344  	if len(headerValue) > 0 {
   345  		headers.Add(headerName, headerValue)
   346  	}
   347  }
   348  
   349  func (adapter *EPlanningAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   350  	if response.StatusCode == http.StatusNoContent {
   351  		return nil, nil
   352  	}
   353  
   354  	if response.StatusCode == http.StatusBadRequest {
   355  		return nil, []error{&errortypes.BadInput{
   356  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   357  		}}
   358  	}
   359  
   360  	if response.StatusCode != http.StatusOK {
   361  		return nil, []error{&errortypes.BadServerResponse{
   362  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   363  		}}
   364  	}
   365  
   366  	var parsedResponse hbResponse
   367  	if err := json.Unmarshal(response.Body, &parsedResponse); err != nil {
   368  		return nil, []error{&errortypes.BadServerResponse{
   369  			Message: fmt.Sprintf("Error unmarshaling HB response: %s", err.Error()),
   370  		}}
   371  	}
   372  
   373  	isMobile := isMobileDevice(internalRequest)
   374  	impType := getImpTypeRequest(internalRequest, len(internalRequest.Imp))
   375  
   376  	bidResponse := adapters.NewBidderResponse()
   377  
   378  	spaceNameToImpID := make(map[string]string)
   379  
   380  	index_vast := 0
   381  	for _, imp := range internalRequest.Imp {
   382  		extImp, err := verifyImp(&imp, isMobile, impType)
   383  		if err != nil {
   384  			continue
   385  		}
   386  
   387  		name := cleanName(extImp.AdUnitCode)
   388  		if imp.Video != nil {
   389  			name = getNameVideo(extImp.SizeString, index_vast)
   390  			index_vast++
   391  		}
   392  		spaceNameToImpID[name] = imp.ID
   393  	}
   394  
   395  	for _, space := range parsedResponse.Spaces {
   396  		for _, ad := range space.Ads {
   397  			if price, err := strconv.ParseFloat(ad.Price, 64); err == nil {
   398  				bid := openrtb2.Bid{
   399  					ID:    ad.ImpressionID,
   400  					AdID:  ad.AdID,
   401  					ImpID: spaceNameToImpID[space.Name],
   402  					Price: price,
   403  					AdM:   ad.AdM,
   404  					CrID:  ad.CrID,
   405  					W:     int64(ad.Width),
   406  					H:     int64(ad.Height),
   407  				}
   408  
   409  				bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   410  					Bid:     &bid,
   411  					BidType: getBidType(impType),
   412  				})
   413  			}
   414  		}
   415  	}
   416  
   417  	return bidResponse, nil
   418  }
   419  
   420  func getBidType(impType int) openrtb_ext.BidType {
   421  
   422  	bidType := openrtb_ext.BidTypeBanner
   423  	if impType > 0 {
   424  		bidType = openrtb_ext.BidTypeVideo
   425  	}
   426  	return bidType
   427  }
   428  
   429  func getNameVideo(size string, index_vast int) string {
   430  	return "video_" + size + "_" + strconv.Itoa(index_vast)
   431  }
   432  
   433  // Builder builds a new instance of the EPlanning adapter for the given bidder with the given config.
   434  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   435  	bidder := &EPlanningAdapter{
   436  		URI:     config.Endpoint,
   437  		testing: false,
   438  	}
   439  	return bidder, nil
   440  }