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