github.com/prebid/prebid-server@v0.275.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/v19/adcom1"
    15  	"github.com/prebid/openrtb/v19/openrtb2"
    16  	"github.com/prebid/prebid-server/adapters"
    17  	"github.com/prebid/prebid-server/config"
    18  	"github.com/prebid/prebid-server/errortypes"
    19  	"github.com/prebid/prebid-server/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  	}
   208  
   209  	requests := []*adapters.RequestData{&requestData}
   210  
   211  	return requests, errors
   212  }
   213  
   214  func isMobileDevice(request *openrtb2.BidRequest) bool {
   215  	return request.Device != nil && (request.Device.DeviceType == adcom1.DeviceMobile || request.Device.DeviceType == adcom1.DevicePhone || request.Device.DeviceType == adcom1.DeviceTablet)
   216  }
   217  
   218  func getImpTypeRequest(request *openrtb2.BidRequest, totalImps int) int {
   219  
   220  	impType := impTypeBanner
   221  	for i := 0; i < totalImps; i++ {
   222  		imp := request.Imp[i]
   223  		if imp.Video != nil {
   224  			if imp.Video.Placement == vastInstream {
   225  				impType = vastInstream
   226  			} else if impType == impTypeBanner {
   227  				impType = vastOutstream
   228  			}
   229  		}
   230  	}
   231  
   232  	return impType
   233  
   234  }
   235  func cleanName(name string) string {
   236  	for _, step := range cleanNameSteps {
   237  		name = step.expression.ReplaceAllString(name, step.replacementString)
   238  	}
   239  	return name
   240  }
   241  
   242  func verifyImp(imp *openrtb2.Imp, isMobile bool, impType int) (*openrtb_ext.ExtImpEPlanning, error) {
   243  	var bidderExt adapters.ExtImpBidder
   244  
   245  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   246  		return nil, &errortypes.BadInput{
   247  			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
   248  		}
   249  	}
   250  
   251  	if impType > impTypeBanner {
   252  		if impType == vastInstream {
   253  			// In-stream
   254  			if imp.Video == nil || imp.Video.Placement != vastInstream {
   255  				return nil, &errortypes.BadInput{
   256  					Message: fmt.Sprintf("Ignoring imp id=%s, auction instream and imp no instream", imp.ID),
   257  				}
   258  			}
   259  		} else {
   260  			//Out-stream
   261  			if imp.Video == nil || imp.Video.Placement == vastInstream {
   262  				return nil, &errortypes.BadInput{
   263  					Message: fmt.Sprintf("Ignoring imp id=%s, auction outstream and imp no outstream", imp.ID),
   264  				}
   265  			}
   266  		}
   267  	}
   268  
   269  	impExt := openrtb_ext.ExtImpEPlanning{}
   270  	err := json.Unmarshal(bidderExt.Bidder, &impExt)
   271  	if err != nil {
   272  		return nil, &errortypes.BadInput{
   273  			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
   274  		}
   275  	}
   276  
   277  	if impExt.ClientID == "" {
   278  		return nil, &errortypes.BadInput{
   279  			Message: fmt.Sprintf("Ignoring imp id=%s, no ClientID present", imp.ID),
   280  		}
   281  	}
   282  
   283  	width, height := getSizeFromImp(imp, isMobile)
   284  
   285  	if width == 0 && height == 0 {
   286  		if imp.Video != nil {
   287  			impExt.SizeString = vastDefaultSize
   288  		} else {
   289  			impExt.SizeString = nullSize
   290  		}
   291  	} else {
   292  		impExt.SizeString = fmt.Sprintf("%dx%d", width, height)
   293  	}
   294  
   295  	if impExt.AdUnitCode == "" {
   296  		impExt.AdUnitCode = impExt.SizeString
   297  	}
   298  
   299  	return &impExt, nil
   300  }
   301  
   302  func searchSizePriority(hashedFormats map[string]int, format []openrtb2.Format, priorityOrderForSizesAsc []string) (int64, int64) {
   303  	for i := len(priorityOrderForSizesAsc) - 1; i >= 0; i-- {
   304  		if formatIndex, wasFound := hashedFormats[priorityOrderForSizesAsc[i]]; wasFound {
   305  			return format[formatIndex].W, format[formatIndex].H
   306  		}
   307  	}
   308  	return format[0].W, format[0].H
   309  }
   310  
   311  func getSizeFromImp(imp *openrtb2.Imp, isMobile bool) (int64, int64) {
   312  
   313  	if imp.Video != nil && imp.Video.W > 0 && imp.Video.H > 0 {
   314  		return imp.Video.W, imp.Video.H
   315  	}
   316  
   317  	if imp.Banner != nil {
   318  		if imp.Banner.W != nil && imp.Banner.H != nil {
   319  			return *imp.Banner.W, *imp.Banner.H
   320  		}
   321  
   322  		if imp.Banner.Format != nil {
   323  			hashedFormats := make(map[string]int, len(imp.Banner.Format))
   324  
   325  			for i, format := range imp.Banner.Format {
   326  				if format.W != 0 && format.H != 0 {
   327  					hashedFormats[fmt.Sprintf("%dx%d", format.W, format.H)] = i
   328  				}
   329  			}
   330  
   331  			if isMobile {
   332  				return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForMobileSizesAsc)
   333  			} else {
   334  				return searchSizePriority(hashedFormats, imp.Banner.Format, priorityOrderForDesktopSizesAsc)
   335  			}
   336  		}
   337  	}
   338  
   339  	return 0, 0
   340  }
   341  
   342  func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) {
   343  	if len(headerValue) > 0 {
   344  		headers.Add(headerName, headerValue)
   345  	}
   346  }
   347  
   348  func (adapter *EPlanningAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   349  	if response.StatusCode == http.StatusNoContent {
   350  		return nil, nil
   351  	}
   352  
   353  	if response.StatusCode == http.StatusBadRequest {
   354  		return nil, []error{&errortypes.BadInput{
   355  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   356  		}}
   357  	}
   358  
   359  	if response.StatusCode != http.StatusOK {
   360  		return nil, []error{&errortypes.BadServerResponse{
   361  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   362  		}}
   363  	}
   364  
   365  	var parsedResponse hbResponse
   366  	if err := json.Unmarshal(response.Body, &parsedResponse); err != nil {
   367  		return nil, []error{&errortypes.BadServerResponse{
   368  			Message: fmt.Sprintf("Error unmarshaling HB response: %s", err.Error()),
   369  		}}
   370  	}
   371  
   372  	isMobile := isMobileDevice(internalRequest)
   373  	impType := getImpTypeRequest(internalRequest, len(internalRequest.Imp))
   374  
   375  	bidResponse := adapters.NewBidderResponse()
   376  
   377  	spaceNameToImpID := make(map[string]string)
   378  
   379  	index_vast := 0
   380  	for _, imp := range internalRequest.Imp {
   381  		extImp, err := verifyImp(&imp, isMobile, impType)
   382  		if err != nil {
   383  			continue
   384  		}
   385  
   386  		name := cleanName(extImp.AdUnitCode)
   387  		if imp.Video != nil {
   388  			name = getNameVideo(extImp.SizeString, index_vast)
   389  			index_vast++
   390  		}
   391  		spaceNameToImpID[name] = imp.ID
   392  	}
   393  
   394  	for _, space := range parsedResponse.Spaces {
   395  		for _, ad := range space.Ads {
   396  			if price, err := strconv.ParseFloat(ad.Price, 64); err == nil {
   397  				bid := openrtb2.Bid{
   398  					ID:    ad.ImpressionID,
   399  					AdID:  ad.AdID,
   400  					ImpID: spaceNameToImpID[space.Name],
   401  					Price: price,
   402  					AdM:   ad.AdM,
   403  					CrID:  ad.CrID,
   404  					W:     int64(ad.Width),
   405  					H:     int64(ad.Height),
   406  				}
   407  
   408  				bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   409  					Bid:     &bid,
   410  					BidType: getBidType(impType),
   411  				})
   412  			}
   413  		}
   414  	}
   415  
   416  	return bidResponse, nil
   417  }
   418  
   419  func getBidType(impType int) openrtb_ext.BidType {
   420  
   421  	bidType := openrtb_ext.BidTypeBanner
   422  	if impType > 0 {
   423  		bidType = openrtb_ext.BidTypeVideo
   424  	}
   425  	return bidType
   426  }
   427  
   428  func getNameVideo(size string, index_vast int) string {
   429  	return "video_" + size + "_" + strconv.Itoa(index_vast)
   430  }
   431  
   432  // Builder builds a new instance of the EPlanning adapter for the given bidder with the given config.
   433  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   434  	bidder := &EPlanningAdapter{
   435  		URI:     config.Endpoint,
   436  		testing: false,
   437  	}
   438  	return bidder, nil
   439  }