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

     1  package ix
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/prebid/prebid-server/v2/adapters"
    11  	"github.com/prebid/prebid-server/v2/config"
    12  	"github.com/prebid/prebid-server/v2/errortypes"
    13  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    14  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    15  	"github.com/prebid/prebid-server/v2/version"
    16  
    17  	"github.com/prebid/openrtb/v20/native1"
    18  	native1response "github.com/prebid/openrtb/v20/native1/response"
    19  	"github.com/prebid/openrtb/v20/openrtb2"
    20  )
    21  
    22  type IxAdapter struct {
    23  	URI string
    24  }
    25  
    26  type ExtRequest struct {
    27  	Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"`
    28  	SChain *openrtb2.SupplyChain         `json:"schain,omitempty"`
    29  	IxDiag *IxDiag                       `json:"ixdiag,omitempty"`
    30  }
    31  
    32  type IxDiag struct {
    33  	PbsV            string `json:"pbsv,omitempty"`
    34  	PbjsV           string `json:"pbjsv,omitempty"`
    35  	MultipleSiteIds string `json:"multipleSiteIds,omitempty"`
    36  }
    37  
    38  type auctionConfig struct {
    39  	BidId  string          `json:"bidId,omitempty"`
    40  	Config json.RawMessage `json:"config,omitempty"`
    41  }
    42  
    43  type ixRespExt struct {
    44  	AuctionConfig []auctionConfig `json:"protectedAudienceAuctionConfigs,omitempty"`
    45  }
    46  
    47  func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    48  	requests := make([]*adapters.RequestData, 0, len(request.Imp))
    49  	errs := make([]error, 0)
    50  
    51  	headers := http.Header{
    52  		"Content-Type": {"application/json;charset=utf-8"},
    53  		"Accept":       {"application/json"}}
    54  
    55  	uniqueSiteIDs := make(map[string]struct{})
    56  	filteredImps := make([]openrtb2.Imp, 0, len(request.Imp))
    57  	requestCopy := *request
    58  
    59  	ixDiag := &IxDiag{}
    60  
    61  	for _, imp := range requestCopy.Imp {
    62  		var err error
    63  		ixExt, err := unmarshalToIxExt(&imp)
    64  
    65  		if err != nil {
    66  			errs = append(errs, err)
    67  			continue
    68  		}
    69  
    70  		if err = parseSiteId(ixExt, uniqueSiteIDs); err != nil {
    71  			errs = append(errs, err)
    72  			continue
    73  		}
    74  
    75  		if err := moveSid(&imp, ixExt); err != nil {
    76  			errs = append(errs, err)
    77  		}
    78  
    79  		if imp.Banner != nil {
    80  			bannerCopy := *imp.Banner
    81  
    82  			if len(bannerCopy.Format) == 0 && bannerCopy.W != nil && bannerCopy.H != nil {
    83  				bannerCopy.Format = []openrtb2.Format{{W: *bannerCopy.W, H: *bannerCopy.H}}
    84  			}
    85  
    86  			if len(bannerCopy.Format) == 1 {
    87  				bannerCopy.W = ptrutil.ToPtr(bannerCopy.Format[0].W)
    88  				bannerCopy.H = ptrutil.ToPtr(bannerCopy.Format[0].H)
    89  			}
    90  			imp.Banner = &bannerCopy
    91  		}
    92  		filteredImps = append(filteredImps, imp)
    93  	}
    94  	requestCopy.Imp = filteredImps
    95  
    96  	setPublisherId(&requestCopy, uniqueSiteIDs, ixDiag)
    97  
    98  	err := setIxDiagIntoExtRequest(&requestCopy, ixDiag)
    99  	if err != nil {
   100  		errs = append(errs, err)
   101  	}
   102  
   103  	if len(requestCopy.Imp) != 0 {
   104  		if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil {
   105  			requests = append(requests, requestData)
   106  		} else {
   107  			errs = append(errs, err)
   108  		}
   109  	}
   110  
   111  	return requests, errs
   112  }
   113  
   114  func setPublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[string]struct{}, ixDiag *IxDiag) {
   115  	siteIDs := make([]string, 0, len(uniqueSiteIDs))
   116  	for key := range uniqueSiteIDs {
   117  		siteIDs = append(siteIDs, key)
   118  	}
   119  	if requestCopy.Site != nil {
   120  		site := *requestCopy.Site
   121  		if site.Publisher == nil {
   122  			site.Publisher = &openrtb2.Publisher{}
   123  		} else {
   124  			publisher := *site.Publisher
   125  			site.Publisher = &publisher
   126  		}
   127  		if len(siteIDs) == 1 {
   128  			site.Publisher.ID = siteIDs[0]
   129  		}
   130  		requestCopy.Site = &site
   131  	}
   132  
   133  	if requestCopy.App != nil {
   134  		app := *requestCopy.App
   135  
   136  		if app.Publisher == nil {
   137  			app.Publisher = &openrtb2.Publisher{}
   138  		} else {
   139  			publisher := *app.Publisher
   140  			app.Publisher = &publisher
   141  		}
   142  		if len(siteIDs) == 1 {
   143  			app.Publisher.ID = siteIDs[0]
   144  		}
   145  		requestCopy.App = &app
   146  	}
   147  
   148  	if len(siteIDs) > 1 {
   149  		// Sorting siteIDs for predictable output as Go maps don't guarantee order
   150  		sort.Strings(siteIDs)
   151  		multipleSiteIDs := strings.Join(siteIDs, ", ")
   152  		ixDiag.MultipleSiteIds = multipleSiteIDs
   153  	}
   154  }
   155  
   156  func unmarshalToIxExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpIx, error) {
   157  	var bidderExt adapters.ExtImpBidder
   158  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	var ixExt openrtb_ext.ExtImpIx
   163  	if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	return &ixExt, nil
   168  }
   169  
   170  func parseSiteId(ixExt *openrtb_ext.ExtImpIx, uniqueSiteIDs map[string]struct{}) error {
   171  	if ixExt == nil {
   172  		return fmt.Errorf("Nil Ix Ext")
   173  	}
   174  	if ixExt.SiteId != "" {
   175  		uniqueSiteIDs[ixExt.SiteId] = struct{}{}
   176  	}
   177  	return nil
   178  }
   179  
   180  func createRequestData(a *IxAdapter, request *openrtb2.BidRequest, headers *http.Header) (*adapters.RequestData, error) {
   181  	body, err := json.Marshal(request)
   182  	return &adapters.RequestData{
   183  		Method:  "POST",
   184  		Uri:     a.URI,
   185  		Body:    body,
   186  		Headers: *headers,
   187  		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   188  	}, err
   189  }
   190  
   191  func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   192  	switch {
   193  	case response.StatusCode == http.StatusNoContent:
   194  		return nil, nil
   195  	case response.StatusCode == http.StatusBadRequest:
   196  		return nil, []error{&errortypes.BadInput{
   197  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   198  		}}
   199  	case response.StatusCode != http.StatusOK:
   200  		return nil, []error{&errortypes.BadServerResponse{
   201  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   202  		}}
   203  	}
   204  
   205  	var bidResponse openrtb2.BidResponse
   206  	if err := json.Unmarshal(response.Body, &bidResponse); err != nil {
   207  		return nil, []error{&errortypes.BadServerResponse{
   208  			Message: fmt.Sprintf("JSON parsing error: %v", err),
   209  		}}
   210  	}
   211  
   212  	// Store media type per impression in a map for later use to set in bid.ext.prebid.type
   213  	// Won't work for multiple bid case with a multi-format ad unit. We expect to get type from exchange on such case.
   214  	impMediaTypeReq := map[string]openrtb_ext.BidType{}
   215  	for _, imp := range internalRequest.Imp {
   216  		if imp.Banner != nil {
   217  			impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeBanner
   218  		} else if imp.Video != nil {
   219  			impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeVideo
   220  		} else if imp.Native != nil {
   221  			impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeNative
   222  		} else if imp.Audio != nil {
   223  			impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeAudio
   224  		}
   225  	}
   226  
   227  	// capacity 0 will make channel unbuffered
   228  	bidderResponse := adapters.NewBidderResponseWithBidsCapacity(0)
   229  	bidderResponse.Currency = bidResponse.Cur
   230  
   231  	var errs []error
   232  
   233  	for _, seatBid := range bidResponse.SeatBid {
   234  		for i := range seatBid.Bid {
   235  			bid := seatBid.Bid[i]
   236  
   237  			bidType, err := getMediaTypeForBid(bid, impMediaTypeReq)
   238  			if err != nil {
   239  				errs = append(errs, err)
   240  				continue
   241  			}
   242  
   243  			var bidExtVideo *openrtb_ext.ExtBidPrebidVideo
   244  			var bidExt openrtb_ext.ExtBid
   245  			if bidType == openrtb_ext.BidTypeVideo {
   246  				unmarshalExtErr := json.Unmarshal(bid.Ext, &bidExt)
   247  				if unmarshalExtErr == nil && bidExt.Prebid != nil && bidExt.Prebid.Video != nil {
   248  					bidExtVideo = &openrtb_ext.ExtBidPrebidVideo{
   249  						Duration: bidExt.Prebid.Video.Duration,
   250  					}
   251  					if len(bid.Cat) == 0 {
   252  						bid.Cat = []string{bidExt.Prebid.Video.PrimaryCategory}
   253  					}
   254  				}
   255  			}
   256  
   257  			var bidNative1v1 *Native11Wrapper
   258  			if bidType == openrtb_ext.BidTypeNative {
   259  				err := json.Unmarshal([]byte(bid.AdM), &bidNative1v1)
   260  				if err == nil && len(bidNative1v1.Native.EventTrackers) > 0 {
   261  					mergeNativeImpTrackers(&bidNative1v1.Native)
   262  					if json, err := marshalJsonWithoutUnicode(bidNative1v1); err == nil {
   263  						bid.AdM = string(json)
   264  					}
   265  				}
   266  			}
   267  
   268  			var bidNative1v2 *native1response.Response
   269  			if bidType == openrtb_ext.BidTypeNative {
   270  				err := json.Unmarshal([]byte(bid.AdM), &bidNative1v2)
   271  				if err == nil && len(bidNative1v2.EventTrackers) > 0 {
   272  					mergeNativeImpTrackers(bidNative1v2)
   273  					if json, err := marshalJsonWithoutUnicode(bidNative1v2); err == nil {
   274  						bid.AdM = string(json)
   275  					}
   276  				}
   277  			}
   278  
   279  			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
   280  				Bid:      &bid,
   281  				BidType:  bidType,
   282  				BidVideo: bidExtVideo,
   283  			})
   284  		}
   285  	}
   286  
   287  	if bidResponse.Ext != nil {
   288  		var bidRespExt ixRespExt
   289  		if err := json.Unmarshal(bidResponse.Ext, &bidRespExt); err != nil {
   290  			return nil, append(errs, err)
   291  		}
   292  
   293  		if bidRespExt.AuctionConfig != nil {
   294  			bidderResponse.FledgeAuctionConfigs = make([]*openrtb_ext.FledgeAuctionConfig, 0, len(bidRespExt.AuctionConfig))
   295  			for _, config := range bidRespExt.AuctionConfig {
   296  				if config.Config != nil {
   297  					fledgeAuctionConfig := &openrtb_ext.FledgeAuctionConfig{
   298  						ImpId:  config.BidId,
   299  						Config: config.Config,
   300  					}
   301  					bidderResponse.FledgeAuctionConfigs = append(bidderResponse.FledgeAuctionConfigs, fledgeAuctionConfig)
   302  				}
   303  			}
   304  		}
   305  	}
   306  
   307  	return bidderResponse, errs
   308  }
   309  
   310  func getMediaTypeForBid(bid openrtb2.Bid, impMediaTypeReq map[string]openrtb_ext.BidType) (openrtb_ext.BidType, error) {
   311  	switch bid.MType {
   312  	case openrtb2.MarkupBanner:
   313  		return openrtb_ext.BidTypeBanner, nil
   314  	case openrtb2.MarkupVideo:
   315  		return openrtb_ext.BidTypeVideo, nil
   316  	case openrtb2.MarkupAudio:
   317  		return openrtb_ext.BidTypeAudio, nil
   318  	case openrtb2.MarkupNative:
   319  		return openrtb_ext.BidTypeNative, nil
   320  	}
   321  
   322  	if bid.Ext != nil {
   323  		var bidExt openrtb_ext.ExtBid
   324  		err := json.Unmarshal(bid.Ext, &bidExt)
   325  		if err == nil && bidExt.Prebid != nil {
   326  			prebidType := string(bidExt.Prebid.Type)
   327  			if prebidType != "" {
   328  				return openrtb_ext.ParseBidType(prebidType)
   329  			}
   330  		}
   331  	}
   332  
   333  	if bidType, ok := impMediaTypeReq[bid.ImpID]; ok {
   334  		return bidType, nil
   335  	} else {
   336  		return "", fmt.Errorf("unmatched impression id: %s", bid.ImpID)
   337  	}
   338  }
   339  
   340  // Builder builds a new instance of the Ix adapter for the given bidder with the given config.
   341  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   342  	bidder := &IxAdapter{
   343  		URI: config.Endpoint,
   344  	}
   345  	return bidder, nil
   346  }
   347  
   348  // native 1.2 to 1.1 tracker compatibility handling
   349  
   350  type Native11Wrapper struct {
   351  	Native native1response.Response `json:"native,omitempty"`
   352  }
   353  
   354  func mergeNativeImpTrackers(bidNative *native1response.Response) {
   355  
   356  	// create unique list of imp pixels urls from `imptrackers` and `eventtrackers`
   357  	uniqueImpPixels := map[string]struct{}{}
   358  	for _, v := range bidNative.ImpTrackers {
   359  		uniqueImpPixels[v] = struct{}{}
   360  	}
   361  
   362  	for _, v := range bidNative.EventTrackers {
   363  		if v.Event == native1.EventTypeImpression && v.Method == native1.EventTrackingMethodImage {
   364  			uniqueImpPixels[v.URL] = struct{}{}
   365  		}
   366  	}
   367  
   368  	// rewrite `imptrackers` with new deduped list of imp pixels
   369  	bidNative.ImpTrackers = make([]string, 0)
   370  	for k := range uniqueImpPixels {
   371  		bidNative.ImpTrackers = append(bidNative.ImpTrackers, k)
   372  	}
   373  
   374  	// sort so tests pass correctly
   375  	sort.Strings(bidNative.ImpTrackers)
   376  }
   377  
   378  func marshalJsonWithoutUnicode(v interface{}) (string, error) {
   379  	// json.Marshal uses HTMLEscape for strings inside JSON which affects URLs
   380  	// this is a problem with Native responses that embed JSON within JSON
   381  	// a custom encoder can be used to disable this encoding.
   382  	// https://pkg.go.dev/encoding/json#Marshal
   383  	// https://pkg.go.dev/encoding/json#Encoder.SetEscapeHTML
   384  	sb := &strings.Builder{}
   385  	encoder := json.NewEncoder(sb)
   386  	encoder.SetEscapeHTML(false)
   387  	if err := encoder.Encode(v); err != nil {
   388  		return "", err
   389  	}
   390  	// json.Encode also writes a newline, need to remove
   391  	// https://pkg.go.dev/encoding/json#Encoder.Encode
   392  	return strings.TrimSuffix(sb.String(), "\n"), nil
   393  }
   394  
   395  func setIxDiagIntoExtRequest(request *openrtb2.BidRequest, ixDiag *IxDiag) error {
   396  	extRequest := &ExtRequest{}
   397  	if request.Ext != nil {
   398  		if err := json.Unmarshal(request.Ext, &extRequest); err != nil {
   399  			return err
   400  		}
   401  	}
   402  
   403  	if extRequest.Prebid != nil && extRequest.Prebid.Channel != nil {
   404  		ixDiag.PbjsV = extRequest.Prebid.Channel.Version
   405  	}
   406  	// Slice commit hash out of version
   407  	if strings.Contains(version.Ver, "-") {
   408  		ixDiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")]
   409  	} else if version.Ver != "" {
   410  		ixDiag.PbsV = version.Ver
   411  	}
   412  
   413  	// Only set request.ext if ixDiag is not empty
   414  	if *ixDiag != (IxDiag{}) {
   415  		extRequest := &ExtRequest{}
   416  		if request.Ext != nil {
   417  			if err := json.Unmarshal(request.Ext, &extRequest); err != nil {
   418  				return err
   419  			}
   420  		}
   421  		extRequest.IxDiag = ixDiag
   422  		extRequestJson, err := json.Marshal(extRequest)
   423  		if err != nil {
   424  			return err
   425  		}
   426  		request.Ext = extRequestJson
   427  	}
   428  	return nil
   429  }
   430  
   431  // moves sid from imp[].ext.bidder.sid to imp[].ext.sid
   432  func moveSid(imp *openrtb2.Imp, ixExt *openrtb_ext.ExtImpIx) error {
   433  	if ixExt == nil {
   434  		return fmt.Errorf("Nil Ix Ext")
   435  	}
   436  
   437  	if ixExt.Sid != "" {
   438  		var m map[string]interface{}
   439  		if err := json.Unmarshal(imp.Ext, &m); err != nil {
   440  			return err
   441  		}
   442  		m["sid"] = ixExt.Sid
   443  		ext, err := json.Marshal(m)
   444  		if err != nil {
   445  			return err
   446  		}
   447  		imp.Ext = ext
   448  	}
   449  	return nil
   450  }