github.com/prebid/prebid-server/v2@v2.18.0/adapters/yandex/yandex.go (about) 1 package yandex 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strconv" 9 "strings" 10 "text/template" 11 12 "github.com/prebid/openrtb/v20/openrtb2" 13 "github.com/prebid/prebid-server/v2/adapters" 14 "github.com/prebid/prebid-server/v2/config" 15 "github.com/prebid/prebid-server/v2/errortypes" 16 "github.com/prebid/prebid-server/v2/macros" 17 "github.com/prebid/prebid-server/v2/openrtb_ext" 18 ) 19 20 const ( 21 refererQueryKey = "target-ref" 22 currencyQueryKey = "ssp-cur" 23 impIdQueryKey = "imp-id" 24 ) 25 26 // Composite id of an ad placement 27 type yandexPlacementID struct { 28 PageID string 29 ImpID string 30 } 31 32 type adapter struct { 33 endpoint *template.Template 34 } 35 36 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 37 template, err := template.New("endpointTemplate").Parse(config.Endpoint) 38 if err != nil { 39 return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) 40 } 41 42 bidder := &adapter{ 43 endpoint: template, 44 } 45 46 return bidder, nil 47 } 48 49 func (a *adapter) MakeRequests(requestData *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 50 var ( 51 requests []*adapters.RequestData 52 errors []error 53 ) 54 55 referer := getReferer(requestData) 56 currency := getCurrency(requestData) 57 58 for i := range requestData.Imp { 59 imp := requestData.Imp[i] 60 61 placementId, err := getYandexPlacementId(imp) 62 if err != nil { 63 errors = append(errors, err) 64 continue 65 } 66 67 if err := modifyImp(&imp); err != nil { 68 errors = append(errors, err) 69 continue 70 } 71 72 resolvedUrl, err := a.resolveUrl(*placementId, referer, currency) 73 if err != nil { 74 errors = append(errors, err) 75 continue 76 } 77 78 splittedRequestData := splitRequestDataByImp(requestData, imp) 79 80 requestBody, err := json.Marshal(splittedRequestData) 81 if err != nil { 82 errors = append(errors, err) 83 continue 84 } 85 86 requests = append(requests, &adapters.RequestData{ 87 Method: "POST", 88 Uri: resolvedUrl, 89 Body: requestBody, 90 Headers: getHeaders(&splittedRequestData), 91 ImpIDs: openrtb_ext.GetImpIDs(splittedRequestData.Imp), 92 }) 93 } 94 95 return requests, errors 96 } 97 98 func getHeaders(request *openrtb2.BidRequest) http.Header { 99 headers := http.Header{} 100 101 if request.Device != nil && request.Site != nil { 102 addNonEmptyHeaders(&headers, map[string]string{ 103 "Referer": request.Site.Page, 104 "Accept-Language": request.Device.Language, 105 "User-Agent": request.Device.UA, 106 "X-Forwarded-For": request.Device.IP, 107 "X-Real-Ip": request.Device.IP, 108 "Content-Type": "application/json;charset=utf-8", 109 "Accept": "application/json", 110 }) 111 } 112 113 return headers 114 } 115 116 func addNonEmptyHeaders(headers *http.Header, headerValues map[string]string) { 117 for key, value := range headerValues { 118 if len(value) > 0 { 119 headers.Add(key, value) 120 } 121 } 122 } 123 124 // Request is in shared memory, so we have to make a shallow copy for further modification (imp is already a shallow copy) 125 func splitRequestDataByImp(request *openrtb2.BidRequest, imp openrtb2.Imp) openrtb2.BidRequest { 126 requestCopy := *request 127 requestCopy.Imp = []openrtb2.Imp{imp} 128 129 return requestCopy 130 } 131 132 func getYandexPlacementId(imp openrtb2.Imp) (*yandexPlacementID, error) { 133 var ext adapters.ExtImpBidder 134 if err := json.Unmarshal(imp.Ext, &ext); err != nil { 135 return nil, &errortypes.BadInput{ 136 Message: fmt.Sprintf("imp %s: unable to unmarshal ext", imp.ID), 137 } 138 } 139 140 var yandexExt openrtb_ext.ExtImpYandex 141 if err := json.Unmarshal(ext.Bidder, &yandexExt); err != nil { 142 return nil, &errortypes.BadInput{ 143 Message: fmt.Sprintf("imp %s: unable to unmarshal ext.bidder: %v", imp.ID, err), 144 } 145 } 146 147 placementID, err := mapExtToPlacementID(yandexExt) 148 if err != nil { 149 return nil, err 150 } 151 152 return placementID, nil 153 } 154 155 func mapExtToPlacementID(yandexExt openrtb_ext.ExtImpYandex) (*yandexPlacementID, error) { 156 var placementID yandexPlacementID 157 158 if len(yandexExt.PlacementID) == 0 { 159 placementID.ImpID = strconv.Itoa(int(yandexExt.ImpID)) 160 placementID.PageID = strconv.Itoa(int(yandexExt.PageID)) 161 return &placementID, nil 162 } 163 164 idParts := strings.Split(yandexExt.PlacementID, "-") 165 166 numericIdParts := []string{} 167 168 for _, idPart := range idParts { 169 if _, err := strconv.Atoi(idPart); err == nil { 170 numericIdParts = append(numericIdParts, idPart) 171 } 172 } 173 174 if len(numericIdParts) < 2 { 175 return nil, &errortypes.BadInput{ 176 Message: fmt.Sprintf("invalid placement id, it must contain two parts: %s", yandexExt.PlacementID), 177 } 178 } 179 180 placementID.ImpID = numericIdParts[len(numericIdParts)-1] 181 placementID.PageID = numericIdParts[len(numericIdParts)-2] 182 183 return &placementID, nil 184 } 185 186 func modifyImp(imp *openrtb2.Imp) error { 187 if imp.Banner != nil { 188 banner, err := modifyBanner(*imp.Banner) 189 if banner != nil { 190 imp.Banner = banner 191 } 192 return err 193 } 194 195 if imp.Native != nil { 196 return nil 197 } 198 199 return &errortypes.BadInput{ 200 Message: fmt.Sprintf("Unsupported format. Yandex only supports banner and native types. Ignoring imp id #%s", imp.ID), 201 } 202 } 203 204 func modifyBanner(banner openrtb2.Banner) (*openrtb2.Banner, error) { 205 format := banner.Format 206 207 if banner.W == nil || banner.H == nil || *banner.W == 0 || *banner.H == 0 { 208 if len(format) == 0 { 209 return nil, &errortypes.BadInput{ 210 Message: "Invalid size provided for Banner", 211 } 212 } 213 214 firstFormat := format[0] 215 banner.H = &firstFormat.H 216 banner.W = &firstFormat.W 217 } 218 219 return &banner, nil 220 } 221 222 // "Un-templates" the endpoint by replacing macroses and adding the required query parameters 223 func (a *adapter) resolveUrl(placementID yandexPlacementID, referer string, currency string) (string, error) { 224 params := macros.EndpointTemplateParams{PageID: placementID.PageID} 225 226 endpointStr, err := macros.ResolveMacros(a.endpoint, params) 227 if err != nil { 228 return "", err 229 } 230 231 parsedUrl, err := url.Parse(endpointStr) 232 if err != nil { 233 return "", err 234 } 235 236 addNonEmptyQueryParams(parsedUrl, map[string]string{ 237 refererQueryKey: referer, 238 currencyQueryKey: currency, 239 impIdQueryKey: placementID.ImpID, 240 }) 241 242 return parsedUrl.String(), nil 243 } 244 245 func addNonEmptyQueryParams(url *url.URL, queryMap map[string]string) { 246 query := url.Query() 247 for key, value := range queryMap { 248 if len(value) > 0 { 249 query.Add(key, value) 250 } 251 } 252 253 url.RawQuery = query.Encode() 254 } 255 256 func getReferer(request *openrtb2.BidRequest) string { 257 if request.Site == nil { 258 return "" 259 } 260 261 return request.Site.Domain 262 } 263 264 func getCurrency(request *openrtb2.BidRequest) string { 265 if len(request.Cur) == 0 { 266 return "" 267 } 268 269 return request.Cur[0] 270 } 271 272 func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { 273 274 if adapters.IsResponseStatusCodeNoContent(responseData) { 275 return nil, nil 276 } 277 278 if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { 279 return nil, []error{err} 280 } 281 282 var bidResponse openrtb2.BidResponse 283 if err := json.Unmarshal(responseData.Body, &bidResponse); err != nil { 284 return nil, []error{&errortypes.BadServerResponse{ 285 Message: fmt.Sprintf("Bad server response: %d", err), 286 }} 287 } 288 289 bidResponseWithCapacity := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) 290 291 var errors []error 292 293 impMap := map[string]*openrtb2.Imp{} 294 for i := range request.Imp { 295 imp := request.Imp[i] 296 297 impMap[imp.ID] = &imp 298 } 299 300 for _, seatBid := range bidResponse.SeatBid { 301 for i := range seatBid.Bid { 302 bid := seatBid.Bid[i] 303 304 imp, exists := impMap[bid.ImpID] 305 if !exists { 306 errors = append(errors, &errortypes.BadInput{ 307 Message: fmt.Sprintf("Invalid bid imp ID #%s does not match any imp IDs from the original bid request", bid.ImpID), 308 }) 309 continue 310 } 311 312 bidType, err := getBidType(*imp) 313 if err != nil { 314 errors = append(errors, err) 315 continue 316 } 317 318 bidResponseWithCapacity.Bids = append(bidResponseWithCapacity.Bids, &adapters.TypedBid{ 319 Bid: &bid, 320 BidType: bidType, 321 }) 322 } 323 } 324 325 return bidResponseWithCapacity, errors 326 } 327 328 func getBidType(imp openrtb2.Imp) (openrtb_ext.BidType, error) { 329 if imp.Native != nil { 330 return openrtb_ext.BidTypeNative, nil 331 } 332 333 if imp.Banner != nil { 334 return openrtb_ext.BidTypeBanner, nil 335 } 336 337 return "", &errortypes.BadInput{ 338 Message: fmt.Sprintf("Processing an invalid impression; cannot resolve impression type for imp #%s", imp.ID), 339 } 340 }