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

     1  package silverpush
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/prebid/openrtb/v20/openrtb2"
     9  	"github.com/prebid/prebid-server/v2/adapters"
    10  	"github.com/prebid/prebid-server/v2/config"
    11  	"github.com/prebid/prebid-server/v2/errortypes"
    12  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    13  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    14  )
    15  
    16  const (
    17  	bidderConfig  = "sp_pb_ortb"
    18  	bidderVersion = "1.0.0"
    19  )
    20  
    21  type adapter struct {
    22  	bidderName string
    23  	endpoint   string
    24  }
    25  
    26  type SilverPushImpExt map[string]json.RawMessage
    27  
    28  type SilverPushReqExt struct {
    29  	PublisherId string  `json:"publisherId"`
    30  	BidFloor    float64 `json:"bidfloor"`
    31  }
    32  
    33  func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    34  	imps := request.Imp
    35  	var errors []error
    36  	requests := make([]*adapters.RequestData, 0, len(imps))
    37  
    38  	for _, imp := range imps {
    39  		impsByMediaType := impressionByMediaType(&imp)
    40  
    41  		request.Imp = []openrtb2.Imp{impsByMediaType}
    42  
    43  		if err := validateRequest(request); err != nil {
    44  			errors = append(errors, err)
    45  			continue
    46  		}
    47  
    48  		requestData, err := a.makeRequest(request)
    49  		if err != nil {
    50  			errors = append(errors, err)
    51  			continue
    52  		}
    53  
    54  		requests = append(requests, requestData)
    55  
    56  	}
    57  	return requests, errors
    58  }
    59  
    60  func (a *adapter) makeRequest(req *openrtb2.BidRequest) (*adapters.RequestData, error) {
    61  	reqJSON, err := json.Marshal(req)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	headers := http.Header{}
    66  	headers.Add("Content-Type", "application/json;charset=utf-8")
    67  	headers.Add("Accept", "application/json")
    68  	headers.Add("X-Openrtb-Version", "2.5")
    69  	return &adapters.RequestData{
    70  		Method:  "POST",
    71  		Uri:     a.endpoint,
    72  		Body:    reqJSON,
    73  		Headers: headers,
    74  		ImpIDs:  openrtb_ext.GetImpIDs(req.Imp),
    75  	}, nil
    76  }
    77  
    78  func validateRequest(req *openrtb2.BidRequest) error {
    79  	imp := &req.Imp[0]
    80  	var silverPushExt openrtb_ext.ImpExtSilverpush
    81  	if err := setPublisherId(req, imp, &silverPushExt); err != nil {
    82  		return err
    83  	}
    84  	if err := setUser(req); err != nil {
    85  		return err
    86  	}
    87  
    88  	setDevice(req)
    89  
    90  	if err := setExtToRequest(req, silverPushExt.PublisherId); err != nil {
    91  		return err
    92  	}
    93  	return setImpForAdExchange(imp, &silverPushExt)
    94  }
    95  
    96  func setDevice(req *openrtb2.BidRequest) {
    97  	if req.Device == nil {
    98  		return
    99  	}
   100  	deviceCopy := *req.Device
   101  	if len(deviceCopy.UA) == 0 {
   102  		return
   103  	}
   104  	deviceCopy.OS = getOS(deviceCopy.UA)
   105  	if isMobile(deviceCopy.UA) {
   106  		deviceCopy.DeviceType = 1
   107  	} else if isCTV(deviceCopy.UA) {
   108  		deviceCopy.DeviceType = 3
   109  	} else {
   110  		deviceCopy.DeviceType = 2
   111  	}
   112  
   113  	req.Device = &deviceCopy
   114  }
   115  
   116  func setUser(req *openrtb2.BidRequest) error {
   117  	var extUser openrtb_ext.ExtUser
   118  	var userExtRaw map[string]json.RawMessage
   119  
   120  	if req.User != nil && req.User.Ext != nil {
   121  		if err := json.Unmarshal(req.User.Ext, &userExtRaw); err != nil {
   122  			return &errortypes.BadInput{Message: "Invalid user.ext."}
   123  		}
   124  		if userExtDataRaw, ok := userExtRaw["data"]; ok {
   125  			if err := json.Unmarshal(userExtDataRaw, &extUser); err != nil {
   126  				return &errortypes.BadInput{Message: "Invalid user.ext.data."}
   127  			}
   128  			var userCopy = *req.User
   129  			if isValidEids(extUser.Eids) {
   130  				userExt, err := json.Marshal(
   131  					&openrtb2.User{
   132  						EIDs: extUser.Eids,
   133  					})
   134  				if err != nil {
   135  					return &errortypes.BadInput{Message: "Error in marshaling user.eids."}
   136  				}
   137  
   138  				userCopy.Ext = userExt
   139  				req.User = &userCopy
   140  			}
   141  		}
   142  	}
   143  	return nil
   144  }
   145  
   146  func setExtToRequest(req *openrtb2.BidRequest, publisherID string) error {
   147  	record := map[string]string{
   148  		"bc":          bidderConfig + "_" + bidderVersion,
   149  		"publisherId": publisherID,
   150  	}
   151  	reqExt, err := json.Marshal(record)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	req.Ext = reqExt
   156  	return nil
   157  }
   158  
   159  func setImpForAdExchange(imp *openrtb2.Imp, impExt *openrtb_ext.ImpExtSilverpush) error {
   160  	if impExt.BidFloor == 0 {
   161  		if imp.Banner != nil {
   162  			imp.BidFloor = 0.05
   163  		} else if imp.Video != nil {
   164  			imp.BidFloor = 0.1
   165  		}
   166  	} else {
   167  		imp.BidFloor = impExt.BidFloor
   168  	}
   169  
   170  	if imp.Banner != nil {
   171  		bannerCopy, err := setBannerDimension(imp.Banner)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		imp.Banner = bannerCopy
   176  	}
   177  
   178  	if imp.Video != nil {
   179  		videoCopy, err := checkVideoDimension(imp.Video)
   180  		if err != nil {
   181  			return err
   182  		}
   183  		imp.Video = videoCopy
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func checkVideoDimension(video *openrtb2.Video) (*openrtb2.Video, error) {
   190  	videoCopy := *video
   191  	if videoCopy.MaxDuration == 0 {
   192  		videoCopy.MaxDuration = 120
   193  	}
   194  	if videoCopy.MaxDuration < videoCopy.MinDuration {
   195  		videoCopy.MaxDuration = videoCopy.MinDuration
   196  		videoCopy.MinDuration = 0
   197  	}
   198  	if videoCopy.API == nil || videoCopy.MIMEs == nil || videoCopy.Protocols == nil || videoCopy.MinDuration < 0 {
   199  		return nil, &errortypes.BadInput{Message: "Invalid or missing video field(s)"}
   200  	}
   201  	return &videoCopy, nil
   202  }
   203  
   204  func setBannerDimension(banner *openrtb2.Banner) (*openrtb2.Banner, error) {
   205  	if banner.W != nil && banner.H != nil {
   206  		return banner, nil
   207  	}
   208  	if len(banner.Format) == 0 {
   209  		return banner, &errortypes.BadInput{Message: "No sizes provided for Banner."}
   210  	}
   211  	bannerCopy := *banner
   212  	bannerCopy.W = ptrutil.ToPtr(banner.Format[0].W)
   213  	bannerCopy.H = ptrutil.ToPtr(banner.Format[0].H)
   214  
   215  	return &bannerCopy, nil
   216  }
   217  
   218  func setPublisherId(req *openrtb2.BidRequest, imp *openrtb2.Imp, impExt *openrtb_ext.ImpExtSilverpush) error {
   219  	var bidderExt adapters.ExtImpBidder
   220  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   221  		return &errortypes.BadInput{
   222  			Message: err.Error(),
   223  		}
   224  	}
   225  
   226  	if err := json.Unmarshal(bidderExt.Bidder, impExt); err != nil {
   227  		return &errortypes.BadInput{
   228  			Message: err.Error(),
   229  		}
   230  	}
   231  	if impExt.PublisherId == "" {
   232  		return &errortypes.BadInput{Message: "Missing publisherId parameter."}
   233  	}
   234  	if req.Site != nil {
   235  		siteCopy := *req.Site
   236  		if siteCopy.Publisher == nil {
   237  			siteCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId}
   238  		} else {
   239  			publisher := *siteCopy.Publisher
   240  			publisher.ID = impExt.PublisherId
   241  			siteCopy.Publisher = &publisher
   242  		}
   243  		req.Site = &siteCopy
   244  
   245  	} else if req.App != nil {
   246  		appCopy := *req.App
   247  		if appCopy.Publisher == nil {
   248  			appCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId}
   249  		} else {
   250  			publisher := *appCopy.Publisher
   251  			publisher.ID = impExt.PublisherId
   252  			appCopy.Publisher = &publisher
   253  		}
   254  		appCopy.Publisher = &openrtb2.Publisher{ID: impExt.PublisherId}
   255  		req.App = &appCopy
   256  
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  func impressionByMediaType(imp *openrtb2.Imp) openrtb2.Imp {
   263  	impCopy := *imp
   264  	if imp.Banner != nil {
   265  		impCopy.Video = nil
   266  	}
   267  	if imp.Video != nil {
   268  		impCopy.Banner = nil
   269  
   270  	}
   271  	return impCopy
   272  }
   273  
   274  func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   275  	if adapters.IsResponseStatusCodeNoContent(response) {
   276  		return nil, nil
   277  	}
   278  	if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
   279  		return nil, []error{&errortypes.BadInput{
   280  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   281  		}}
   282  	}
   283  
   284  	var bidResp openrtb2.BidResponse
   285  
   286  	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
   287  		return nil, []error{err}
   288  	}
   289  
   290  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp))
   291  
   292  	// overrride default currency
   293  	if bidResp.Cur != "" {
   294  		bidResponse.Currency = bidResp.Cur
   295  	}
   296  
   297  	for _, sb := range bidResp.SeatBid {
   298  		for i := 0; i < len(sb.Bid); i++ {
   299  			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   300  				Bid:     &sb.Bid[i],
   301  				BidType: getMediaTypeForImp(sb.Bid[i]),
   302  			})
   303  		}
   304  	}
   305  	return bidResponse, nil
   306  }
   307  
   308  // getMediaTypeForImp figures out which media type this bid is for.
   309  // SilverPush doesn't support multi-type impressions.
   310  // If both banner and video exist, take banner as we do not want in-banner video.
   311  func getMediaTypeForImp(bid openrtb2.Bid) openrtb_ext.BidType {
   312  	switch bid.MType {
   313  	case openrtb2.MarkupBanner:
   314  		return openrtb_ext.BidTypeBanner
   315  	case openrtb2.MarkupVideo:
   316  		return openrtb_ext.BidTypeVideo
   317  	default:
   318  		return ""
   319  	}
   320  }
   321  
   322  // Builder builds a new instance of the silverpush adapter for the given bidder with the given config.
   323  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   324  	bidder := &adapter{
   325  		endpoint:   config.Endpoint,
   326  		bidderName: string(bidderName),
   327  	}
   328  	return bidder, nil
   329  }