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 }