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

     1  package smaato
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/buger/jsonparser"
    11  	"github.com/prebid/openrtb/v20/openrtb2"
    12  	"github.com/prebid/prebid-server/v2/adapters"
    13  	"github.com/prebid/prebid-server/v2/config"
    14  	"github.com/prebid/prebid-server/v2/errortypes"
    15  	"github.com/prebid/prebid-server/v2/metrics"
    16  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    17  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    18  	"github.com/prebid/prebid-server/v2/util/timeutil"
    19  )
    20  
    21  const clientVersion = "prebid_server_0.6"
    22  
    23  type adMarkupType string
    24  
    25  const (
    26  	smtAdTypeImg       adMarkupType = "Img"
    27  	smtAdTypeRichmedia adMarkupType = "Richmedia"
    28  	smtAdTypeVideo     adMarkupType = "Video"
    29  	smtAdTypeNative    adMarkupType = "Native"
    30  )
    31  
    32  // adapter describes a Smaato prebid server adapter.
    33  type adapter struct {
    34  	clock    timeutil.Time
    35  	endpoint string
    36  }
    37  
    38  // userExtData defines User.Ext.Data object for Smaato
    39  type userExtData struct {
    40  	Keywords string `json:"keywords"`
    41  	Gender   string `json:"gender"`
    42  	Yob      int64  `json:"yob"`
    43  }
    44  
    45  // siteExt defines Site.Ext object for Smaato
    46  type siteExt struct {
    47  	Data siteExtData `json:"data"`
    48  }
    49  
    50  type siteExtData struct {
    51  	Keywords string `json:"keywords"`
    52  }
    53  
    54  // bidRequestExt defines BidRequest.Ext object for Smaato
    55  type bidRequestExt struct {
    56  	Client string `json:"client"`
    57  }
    58  
    59  // bidExt defines Bid.Ext object for Smaato
    60  type bidExt struct {
    61  	Duration int `json:"duration"`
    62  }
    63  
    64  // videoExt defines Video.Ext object for Smaato
    65  type videoExt struct {
    66  	Context string `json:"context,omitempty"`
    67  }
    68  
    69  // Builder builds a new instance of the Smaato adapter for the given bidder with the given config.
    70  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    71  	bidder := &adapter{
    72  		clock:    &timeutil.RealTime{},
    73  		endpoint: config.Endpoint,
    74  	}
    75  	return bidder, nil
    76  }
    77  
    78  // MakeRequests makes the HTTP requests which should be made to fetch bids.
    79  func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    80  	if len(request.Imp) == 0 {
    81  		return nil, []error{&errortypes.BadInput{Message: "No impressions in bid request."}}
    82  	}
    83  
    84  	// set data in request that is common for all requests
    85  	if err := prepareCommonRequest(request); err != nil {
    86  		return nil, []error{err}
    87  	}
    88  
    89  	isVideoEntryPoint := reqInfo.PbsEntryPoint == metrics.ReqTypeVideo
    90  
    91  	if isVideoEntryPoint {
    92  		return adapter.makePodRequests(request)
    93  	} else {
    94  		return adapter.makeIndividualRequests(request)
    95  	}
    96  }
    97  
    98  // MakeBids unpacks the server's response into Bids.
    99  func (adapter *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   100  	if response.StatusCode == http.StatusNoContent {
   101  		return nil, nil
   102  	}
   103  
   104  	if response.StatusCode != http.StatusOK {
   105  		return nil, []error{&errortypes.BadServerResponse{
   106  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", response.StatusCode),
   107  		}}
   108  	}
   109  
   110  	var bidResp openrtb2.BidResponse
   111  	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
   112  		return nil, []error{err}
   113  	}
   114  
   115  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)
   116  
   117  	var errors []error
   118  	for _, seatBid := range bidResp.SeatBid {
   119  		for i := 0; i < len(seatBid.Bid); i++ {
   120  			bid := seatBid.Bid[i]
   121  
   122  			adMarkupType, err := getAdMarkupType(response, bid.AdM)
   123  			if err != nil {
   124  				errors = append(errors, err)
   125  				continue
   126  			}
   127  
   128  			bid.AdM, err = renderAdMarkup(adMarkupType, bid.AdM)
   129  			if err != nil {
   130  				errors = append(errors, err)
   131  				continue
   132  			}
   133  
   134  			bidType, err := convertAdMarkupTypeToMediaType(adMarkupType)
   135  			if err != nil {
   136  				errors = append(errors, err)
   137  				continue
   138  			}
   139  
   140  			bidVideo, err := buildBidVideo(&bid, bidType)
   141  			if err != nil {
   142  				errors = append(errors, err)
   143  				continue
   144  			}
   145  
   146  			bid.Exp = adapter.getTTLFromHeaderOrDefault(response)
   147  
   148  			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   149  				Bid:      &bid,
   150  				BidType:  bidType,
   151  				BidVideo: bidVideo,
   152  			})
   153  		}
   154  	}
   155  	return bidResponse, errors
   156  }
   157  
   158  func (adapter *adapter) makeIndividualRequests(request *openrtb2.BidRequest) ([]*adapters.RequestData, []error) {
   159  	imps := request.Imp
   160  
   161  	requests := make([]*adapters.RequestData, 0, len(imps))
   162  	errors := make([]error, 0, len(imps))
   163  
   164  	for _, imp := range imps {
   165  		impsByMediaType, err := splitImpressionsByMediaType(&imp)
   166  		if err != nil {
   167  			errors = append(errors, err)
   168  			continue
   169  		}
   170  
   171  		for _, impByMediaType := range impsByMediaType {
   172  			request.Imp = []openrtb2.Imp{impByMediaType}
   173  			if err := prepareIndividualRequest(request); err != nil {
   174  				errors = append(errors, err)
   175  				continue
   176  			}
   177  
   178  			requestData, err := adapter.makeRequest(request)
   179  			if err != nil {
   180  				errors = append(errors, err)
   181  				continue
   182  			}
   183  
   184  			requests = append(requests, requestData)
   185  		}
   186  	}
   187  
   188  	return requests, errors
   189  }
   190  
   191  func splitImpressionsByMediaType(imp *openrtb2.Imp) ([]openrtb2.Imp, error) {
   192  	if imp.Banner == nil && imp.Video == nil && imp.Native == nil {
   193  		return nil, &errortypes.BadInput{Message: "Invalid MediaType. Smaato only supports Banner, Video and Native."}
   194  	}
   195  
   196  	imps := make([]openrtb2.Imp, 0, 3)
   197  
   198  	if imp.Banner != nil {
   199  		impCopy := *imp
   200  		impCopy.Video = nil
   201  		impCopy.Native = nil
   202  		imps = append(imps, impCopy)
   203  	}
   204  
   205  	if imp.Video != nil {
   206  		impCopy := *imp
   207  		impCopy.Banner = nil
   208  		impCopy.Native = nil
   209  		imps = append(imps, impCopy)
   210  	}
   211  
   212  	if imp.Native != nil {
   213  		imp.Banner = nil
   214  		imp.Video = nil
   215  		imps = append(imps, *imp)
   216  	}
   217  
   218  	return imps, nil
   219  }
   220  
   221  func (adapter *adapter) makePodRequests(request *openrtb2.BidRequest) ([]*adapters.RequestData, []error) {
   222  	pods, orderedKeys, errors := groupImpressionsByPod(request.Imp)
   223  	requests := make([]*adapters.RequestData, 0, len(pods))
   224  
   225  	for _, key := range orderedKeys {
   226  		request.Imp = pods[key]
   227  
   228  		if err := preparePodRequest(request); err != nil {
   229  			errors = append(errors, err)
   230  			continue
   231  		}
   232  
   233  		requestData, err := adapter.makeRequest(request)
   234  		if err != nil {
   235  			errors = append(errors, err)
   236  			continue
   237  		}
   238  
   239  		requests = append(requests, requestData)
   240  	}
   241  
   242  	return requests, errors
   243  }
   244  
   245  func (adapter *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) {
   246  	reqJSON, err := json.Marshal(request)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	headers := http.Header{}
   252  	headers.Add("Content-Type", "application/json;charset=utf-8")
   253  	headers.Add("Accept", "application/json")
   254  
   255  	return &adapters.RequestData{
   256  		Method:  "POST",
   257  		Uri:     adapter.endpoint,
   258  		Body:    reqJSON,
   259  		Headers: headers,
   260  		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   261  	}, nil
   262  }
   263  
   264  func getAdMarkupType(response *adapters.ResponseData, adMarkup string) (adMarkupType, error) {
   265  	if admType := adMarkupType(response.Headers.Get("X-Smt-Adtype")); admType != "" {
   266  		return admType, nil
   267  	} else if strings.HasPrefix(adMarkup, `{"image":`) {
   268  		return smtAdTypeImg, nil
   269  	} else if strings.HasPrefix(adMarkup, `{"richmedia":`) {
   270  		return smtAdTypeRichmedia, nil
   271  	} else if strings.HasPrefix(adMarkup, `<?xml`) {
   272  		return smtAdTypeVideo, nil
   273  	} else if strings.HasPrefix(adMarkup, `{"native":`) {
   274  		return smtAdTypeNative, nil
   275  	} else {
   276  		return "", &errortypes.BadServerResponse{
   277  			Message: fmt.Sprintf("Invalid ad markup %s.", adMarkup),
   278  		}
   279  	}
   280  }
   281  
   282  func (adapter *adapter) getTTLFromHeaderOrDefault(response *adapters.ResponseData) int64 {
   283  	ttl := int64(300)
   284  
   285  	if expiresAtMillis, err := strconv.ParseInt(response.Headers.Get("X-Smt-Expires"), 10, 64); err == nil {
   286  		nowMillis := adapter.clock.Now().UnixNano() / 1000000
   287  		ttl = (expiresAtMillis - nowMillis) / 1000
   288  		if ttl < 0 {
   289  			ttl = 0
   290  		}
   291  	}
   292  
   293  	return ttl
   294  }
   295  
   296  func renderAdMarkup(adMarkupType adMarkupType, adMarkup string) (string, error) {
   297  	switch adMarkupType {
   298  	case smtAdTypeImg:
   299  		return extractAdmImage(adMarkup)
   300  	case smtAdTypeRichmedia:
   301  		return extractAdmRichMedia(adMarkup)
   302  	case smtAdTypeVideo:
   303  		return adMarkup, nil
   304  	case smtAdTypeNative:
   305  		return extractAdmNative(adMarkup)
   306  	default:
   307  		return "", &errortypes.BadServerResponse{
   308  			Message: fmt.Sprintf("Unknown markup type %s.", adMarkupType),
   309  		}
   310  	}
   311  }
   312  
   313  func convertAdMarkupTypeToMediaType(adMarkupType adMarkupType) (openrtb_ext.BidType, error) {
   314  	switch adMarkupType {
   315  	case smtAdTypeImg:
   316  		return openrtb_ext.BidTypeBanner, nil
   317  	case smtAdTypeRichmedia:
   318  		return openrtb_ext.BidTypeBanner, nil
   319  	case smtAdTypeVideo:
   320  		return openrtb_ext.BidTypeVideo, nil
   321  	case smtAdTypeNative:
   322  		return openrtb_ext.BidTypeNative, nil
   323  	default:
   324  		return "", &errortypes.BadServerResponse{
   325  			Message: fmt.Sprintf("Unknown markup type %s.", adMarkupType),
   326  		}
   327  	}
   328  }
   329  
   330  func prepareCommonRequest(request *openrtb2.BidRequest) error {
   331  	if err := setUser(request); err != nil {
   332  		return err
   333  	}
   334  
   335  	if err := setSite(request); err != nil {
   336  		return err
   337  	}
   338  
   339  	setApp(request)
   340  
   341  	return setExt(request)
   342  }
   343  
   344  func prepareIndividualRequest(request *openrtb2.BidRequest) error {
   345  	imp := &request.Imp[0]
   346  
   347  	if err := setPublisherId(request, imp); err != nil {
   348  		return err
   349  	}
   350  
   351  	return setImpForAdspace(imp)
   352  }
   353  
   354  func preparePodRequest(request *openrtb2.BidRequest) error {
   355  	if len(request.Imp) < 1 {
   356  		return &errortypes.BadInput{Message: "No impressions in bid request."}
   357  	}
   358  
   359  	if err := setPublisherId(request, &request.Imp[0]); err != nil {
   360  		return err
   361  	}
   362  
   363  	return setImpForAdBreak(request.Imp)
   364  }
   365  
   366  func setUser(request *openrtb2.BidRequest) error {
   367  	if request.User != nil && request.User.Ext != nil {
   368  		var userExtRaw map[string]json.RawMessage
   369  
   370  		if err := json.Unmarshal(request.User.Ext, &userExtRaw); err != nil {
   371  			return &errortypes.BadInput{Message: "Invalid user.ext."}
   372  		}
   373  
   374  		if userExtDataRaw, present := userExtRaw["data"]; present {
   375  			var err error
   376  			var userExtData userExtData
   377  
   378  			if err = json.Unmarshal(userExtDataRaw, &userExtData); err != nil {
   379  				return &errortypes.BadInput{Message: "Invalid user.ext.data."}
   380  			}
   381  
   382  			userCopy := *request.User
   383  
   384  			if userExtData.Gender != "" {
   385  				userCopy.Gender = userExtData.Gender
   386  			}
   387  
   388  			if userExtData.Yob != 0 {
   389  				userCopy.Yob = userExtData.Yob
   390  			}
   391  
   392  			if userExtData.Keywords != "" {
   393  				userCopy.Keywords = userExtData.Keywords
   394  			}
   395  
   396  			delete(userExtRaw, "data")
   397  
   398  			if userCopy.Ext, err = json.Marshal(userExtRaw); err != nil {
   399  				return err
   400  			}
   401  
   402  			request.User = &userCopy
   403  		}
   404  	}
   405  
   406  	return nil
   407  }
   408  
   409  func setExt(request *openrtb2.BidRequest) error {
   410  	var err error
   411  
   412  	request.Ext, err = json.Marshal(bidRequestExt{Client: clientVersion})
   413  
   414  	return err
   415  }
   416  
   417  func setSite(request *openrtb2.BidRequest) error {
   418  	if request.Site != nil {
   419  		siteCopy := *request.Site
   420  
   421  		if request.Site.Ext != nil {
   422  			var siteExt siteExt
   423  
   424  			if err := json.Unmarshal(request.Site.Ext, &siteExt); err != nil {
   425  				return &errortypes.BadInput{Message: "Invalid site.ext."}
   426  			}
   427  
   428  			siteCopy.Keywords = siteExt.Data.Keywords
   429  			siteCopy.Ext = nil
   430  		}
   431  		request.Site = &siteCopy
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  func setApp(request *openrtb2.BidRequest) {
   438  	if request.App != nil {
   439  		appCopy := *request.App
   440  		request.App = &appCopy
   441  	}
   442  }
   443  
   444  func setPublisherId(request *openrtb2.BidRequest, imp *openrtb2.Imp) error {
   445  	publisherID, err := jsonparser.GetString(imp.Ext, "bidder", "publisherId")
   446  	if err != nil {
   447  		return &errortypes.BadInput{Message: "Missing publisherId parameter."}
   448  	}
   449  
   450  	if request.Site != nil {
   451  		// Site is already a copy
   452  		request.Site.Publisher = &openrtb2.Publisher{ID: publisherID}
   453  		return nil
   454  	} else if request.App != nil {
   455  		// App is already a copy
   456  		request.App.Publisher = &openrtb2.Publisher{ID: publisherID}
   457  		return nil
   458  	} else {
   459  		return &errortypes.BadInput{Message: "Missing Site/App."}
   460  	}
   461  }
   462  
   463  func setImpForAdspace(imp *openrtb2.Imp) error {
   464  	adSpaceID, err := jsonparser.GetString(imp.Ext, "bidder", "adspaceId")
   465  	if err != nil {
   466  		return &errortypes.BadInput{Message: "Missing adspaceId parameter."}
   467  	}
   468  
   469  	impExt, err := makeImpExt(&imp.Ext)
   470  	if err != nil {
   471  		return err
   472  	}
   473  
   474  	if imp.Banner != nil {
   475  		bannerCopy, err := setBannerDimension(imp.Banner)
   476  		if err != nil {
   477  			return err
   478  		}
   479  		imp.Banner = bannerCopy
   480  		imp.TagID = adSpaceID
   481  		imp.Ext = impExt
   482  		return nil
   483  	}
   484  
   485  	if imp.Video != nil || imp.Native != nil {
   486  		imp.TagID = adSpaceID
   487  		imp.Ext = impExt
   488  		return nil
   489  	}
   490  
   491  	return nil
   492  }
   493  
   494  func setImpForAdBreak(imps []openrtb2.Imp) error {
   495  	if len(imps) < 1 {
   496  		return &errortypes.BadInput{Message: "No impressions in bid request."}
   497  	}
   498  
   499  	adBreakID, err := jsonparser.GetString(imps[0].Ext, "bidder", "adbreakId")
   500  	if err != nil {
   501  		return &errortypes.BadInput{Message: "Missing adbreakId parameter."}
   502  	}
   503  
   504  	impExt, err := makeImpExt(&imps[0].Ext)
   505  	if err != nil {
   506  		return err
   507  	}
   508  
   509  	for i := range imps {
   510  		imps[i].TagID = adBreakID
   511  		imps[i].Ext = nil
   512  
   513  		videoCopy := *(imps[i].Video)
   514  
   515  		videoCopy.Sequence = int8(i + 1)
   516  		videoCopy.Ext, _ = json.Marshal(&videoExt{Context: "adpod"})
   517  
   518  		imps[i].Video = &videoCopy
   519  	}
   520  
   521  	imps[0].Ext = impExt
   522  
   523  	return nil
   524  }
   525  
   526  func makeImpExt(impExtRaw *json.RawMessage) (json.RawMessage, error) {
   527  	var impExt openrtb_ext.ExtImpExtraDataSmaato
   528  
   529  	if err := json.Unmarshal(*impExtRaw, &impExt); err != nil {
   530  		return nil, &errortypes.BadInput{Message: "Invalid imp.ext."}
   531  	}
   532  
   533  	if impExtSkadnRaw := impExt.Skadn; impExtSkadnRaw != nil {
   534  		var impExtSkadn map[string]json.RawMessage
   535  
   536  		if err := json.Unmarshal(impExtSkadnRaw, &impExtSkadn); err != nil {
   537  			return nil, &errortypes.BadInput{Message: "Invalid imp.ext.skadn."}
   538  		}
   539  	}
   540  
   541  	if impExtJson, err := json.Marshal(impExt); string(impExtJson) != "{}" {
   542  		return impExtJson, err
   543  	} else {
   544  		return nil, nil
   545  	}
   546  }
   547  
   548  func setBannerDimension(banner *openrtb2.Banner) (*openrtb2.Banner, error) {
   549  	if banner.W != nil && banner.H != nil {
   550  		return banner, nil
   551  	}
   552  	if len(banner.Format) == 0 {
   553  		return banner, &errortypes.BadInput{Message: "No sizes provided for Banner."}
   554  	}
   555  	bannerCopy := *banner
   556  	bannerCopy.W = ptrutil.ToPtr(banner.Format[0].W)
   557  	bannerCopy.H = ptrutil.ToPtr(banner.Format[0].H)
   558  
   559  	return &bannerCopy, nil
   560  }
   561  
   562  func groupImpressionsByPod(imps []openrtb2.Imp) (map[string]([]openrtb2.Imp), []string, []error) {
   563  	pods := make(map[string][]openrtb2.Imp)
   564  	orderKeys := make([]string, 0)
   565  	errors := make([]error, 0, len(imps))
   566  
   567  	for _, imp := range imps {
   568  		if imp.Video == nil {
   569  			errors = append(errors, &errortypes.BadInput{Message: "Invalid MediaType. Smaato only supports Video for AdPod."})
   570  			continue
   571  		}
   572  
   573  		pod := strings.Split(imp.ID, "_")[0]
   574  		if _, present := pods[pod]; !present {
   575  			orderKeys = append(orderKeys, pod)
   576  		}
   577  		pods[pod] = append(pods[pod], imp)
   578  	}
   579  	return pods, orderKeys, errors
   580  }
   581  
   582  func buildBidVideo(bid *openrtb2.Bid, bidType openrtb_ext.BidType) (*openrtb_ext.ExtBidPrebidVideo, error) {
   583  	if bidType != openrtb_ext.BidTypeVideo {
   584  		return nil, nil
   585  	}
   586  
   587  	if bid.Ext == nil {
   588  		return nil, nil
   589  	}
   590  
   591  	var primaryCategory string
   592  	if len(bid.Cat) > 0 {
   593  		primaryCategory = bid.Cat[0]
   594  	}
   595  
   596  	var bidExt bidExt
   597  	if err := json.Unmarshal(bid.Ext, &bidExt); err != nil {
   598  		return nil, &errortypes.BadServerResponse{Message: "Invalid bid.ext."}
   599  	}
   600  
   601  	return &openrtb_ext.ExtBidPrebidVideo{
   602  		Duration:        bidExt.Duration,
   603  		PrimaryCategory: primaryCategory,
   604  	}, nil
   605  }