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