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