github.com/prebid/prebid-server/v2@v2.18.0/adapters/adkernel/adkernel.go (about) 1 package adkernel 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "strconv" 8 "strings" 9 "text/template" 10 11 "github.com/prebid/openrtb/v20/openrtb2" 12 "github.com/prebid/prebid-server/v2/adapters" 13 "github.com/prebid/prebid-server/v2/config" 14 "github.com/prebid/prebid-server/v2/errortypes" 15 "github.com/prebid/prebid-server/v2/macros" 16 "github.com/prebid/prebid-server/v2/openrtb_ext" 17 ) 18 19 const ( 20 mf_suffix = "__mf" 21 mf_suffix_banner = "b" + mf_suffix 22 mf_suffix_video = "v" + mf_suffix 23 mf_suffix_audio = "a" + mf_suffix 24 mf_suffix_native = "n" + mf_suffix 25 ) 26 27 type adkernelAdapter struct { 28 EndpointTemplate *template.Template 29 } 30 31 // MakeRequests prepares request information for prebid-server core 32 func (adapter *adkernelAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 33 errs := make([]error, 0, len(request.Imp)) 34 if len(request.Imp) == 0 { 35 errs = append(errs, newBadInputError("No impression in the bid request")) 36 return nil, errs 37 } 38 imps, impExts, err := getImpressionsInfo(request.Imp) 39 if len(imps) == 0 { 40 return nil, err 41 } 42 errs = append(errs, err...) 43 44 pub2impressions, dispErrors := dispatchImpressions(imps, impExts) 45 if len(dispErrors) > 0 { 46 errs = append(errs, dispErrors...) 47 } 48 if len(pub2impressions) == 0 { 49 return nil, errs 50 } 51 result := make([]*adapters.RequestData, 0, len(pub2impressions)) 52 for k, imps := range pub2impressions { 53 bidRequest, err := adapter.buildAdapterRequest(request, &k, imps) 54 if err != nil { 55 errs = append(errs, err) 56 } else { 57 result = append(result, bidRequest) 58 } 59 } 60 return result, errs 61 } 62 63 // getImpressionsInfo checks each impression for validity and returns impressions copy with corresponding exts 64 func getImpressionsInfo(imps []openrtb2.Imp) ([]openrtb2.Imp, []openrtb_ext.ExtImpAdkernel, []error) { 65 impsCount := len(imps) 66 errors := make([]error, 0, impsCount) 67 resImps := make([]openrtb2.Imp, 0, impsCount) 68 resImpExts := make([]openrtb_ext.ExtImpAdkernel, 0, impsCount) 69 70 for _, imp := range imps { 71 impExt, err := getImpressionExt(&imp) 72 if err != nil { 73 errors = append(errors, err) 74 continue 75 } 76 if err := validateImpression(&imp, impExt); err != nil { 77 errors = append(errors, err) 78 continue 79 } 80 resImps = append(resImps, imp) 81 resImpExts = append(resImpExts, *impExt) 82 } 83 return resImps, resImpExts, errors 84 } 85 86 func validateImpression(imp *openrtb2.Imp, impExt *openrtb_ext.ExtImpAdkernel) error { 87 if impExt.ZoneId < 1 { 88 return newBadInputError(fmt.Sprintf("Invalid zoneId value: %d. Ignoring imp id=%s", impExt.ZoneId, imp.ID)) 89 } 90 return nil 91 } 92 93 // Group impressions by AdKernel-specific parameter `zoneId` 94 func dispatchImpressions(imps []openrtb2.Imp, impsExt []openrtb_ext.ExtImpAdkernel) (map[openrtb_ext.ExtImpAdkernel][]openrtb2.Imp, []error) { 95 res := make(map[openrtb_ext.ExtImpAdkernel][]openrtb2.Imp) 96 errors := make([]error, 0) 97 for idx := range imps { 98 imp := imps[idx] 99 imp.Ext = nil 100 impExt := impsExt[idx] 101 if res[impExt] == nil { 102 res[impExt] = make([]openrtb2.Imp, 0) 103 } 104 if isMultiFormatImp(&imp) { 105 splImps := splitMultiFormatImp(&imp) 106 res[impExt] = append(res[impExt], splImps...) 107 } else { 108 res[impExt] = append(res[impExt], imp) 109 } 110 } 111 return res, errors 112 } 113 114 func isMultiFormatImp(imp *openrtb2.Imp) bool { 115 count := 0 116 if imp.Video != nil { 117 count++ 118 } 119 if imp.Audio != nil { 120 count++ 121 } 122 if imp.Banner != nil { 123 count++ 124 } 125 if imp.Native != nil { 126 count++ 127 } 128 return count > 1 129 } 130 131 func splitMultiFormatImp(imp *openrtb2.Imp) []openrtb2.Imp { 132 splitImps := make([]openrtb2.Imp, 0, 4) 133 if imp.Banner != nil { 134 impCopy := *imp 135 impCopy.Video = nil 136 impCopy.Native = nil 137 impCopy.Audio = nil 138 impCopy.ID += mf_suffix_banner 139 splitImps = append(splitImps, impCopy) 140 } 141 if imp.Video != nil { 142 impCopy := *imp 143 impCopy.Banner = nil 144 impCopy.Native = nil 145 impCopy.Audio = nil 146 impCopy.ID += mf_suffix_video 147 splitImps = append(splitImps, impCopy) 148 } 149 150 if imp.Native != nil { 151 impCopy := *imp 152 impCopy.Banner = nil 153 impCopy.Video = nil 154 impCopy.Audio = nil 155 impCopy.ID += mf_suffix_native 156 splitImps = append(splitImps, impCopy) 157 } 158 159 if imp.Audio != nil { 160 impCopy := *imp 161 impCopy.Banner = nil 162 impCopy.Video = nil 163 impCopy.Native = nil 164 impCopy.ID += mf_suffix_audio 165 splitImps = append(splitImps, impCopy) 166 } 167 return splitImps 168 } 169 170 func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpAdkernel, error) { 171 var bidderExt adapters.ExtImpBidder 172 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 173 return nil, &errortypes.BadInput{ 174 Message: err.Error(), 175 } 176 } 177 var adkernelExt openrtb_ext.ExtImpAdkernel 178 if err := json.Unmarshal(bidderExt.Bidder, &adkernelExt); err != nil { 179 return nil, &errortypes.BadInput{ 180 Message: err.Error(), 181 } 182 } 183 return &adkernelExt, nil 184 } 185 186 func (adapter *adkernelAdapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpAdkernel, imps []openrtb2.Imp) (*adapters.RequestData, error) { 187 newBidRequest := createBidRequest(prebidBidRequest, params, imps) 188 reqJSON, err := json.Marshal(newBidRequest) 189 if err != nil { 190 return nil, err 191 } 192 193 headers := http.Header{} 194 headers.Add("Content-Type", "application/json;charset=utf-8") 195 headers.Add("Accept", "application/json") 196 headers.Add("x-openrtb-version", "2.5") 197 198 url, err := adapter.buildEndpointURL(params) 199 if err != nil { 200 return nil, err 201 } 202 203 return &adapters.RequestData{ 204 Method: "POST", 205 Uri: url, 206 Body: reqJSON, 207 Headers: headers, 208 ImpIDs: openrtb_ext.GetImpIDs(imps)}, nil 209 } 210 211 func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpAdkernel, imps []openrtb2.Imp) *openrtb2.BidRequest { 212 bidRequest := *prebidBidRequest 213 bidRequest.Imp = imps 214 if bidRequest.Site != nil { 215 // Need to copy Site as Request is a shallow copy 216 siteCopy := *bidRequest.Site 217 bidRequest.Site = &siteCopy 218 bidRequest.Site.Publisher = nil 219 } 220 if bidRequest.App != nil { 221 // Need to copy App as Request is a shallow copy 222 appCopy := *bidRequest.App 223 bidRequest.App = &appCopy 224 bidRequest.App.Publisher = nil 225 } 226 return &bidRequest 227 } 228 229 // Builds endpoint url based on adapter-specific pub settings from imp.ext 230 func (adapter *adkernelAdapter) buildEndpointURL(params *openrtb_ext.ExtImpAdkernel) (string, error) { 231 endpointParams := macros.EndpointTemplateParams{ZoneID: strconv.Itoa(params.ZoneId)} 232 return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams) 233 } 234 235 // MakeBids translates adkernel bid response to prebid-server specific format 236 func (adapter *adkernelAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 237 if response.StatusCode == http.StatusNoContent { 238 return nil, nil 239 } 240 if response.StatusCode != http.StatusOK { 241 return nil, []error{ 242 newBadServerResponseError(fmt.Sprintf("Unexpected http status code: %d", response.StatusCode)), 243 } 244 } 245 var bidResp openrtb2.BidResponse 246 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 247 return nil, []error{ 248 newBadServerResponseError(fmt.Sprintf("Bad server response: %d", err)), 249 } 250 } 251 252 if len(bidResp.SeatBid) != 1 { 253 return nil, []error{ 254 newBadServerResponseError(fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid))), 255 } 256 } 257 258 seatBid := bidResp.SeatBid[0] 259 bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid)) 260 bidResponse.Currency = bidResp.Cur 261 for i := 0; i < len(seatBid.Bid); i++ { 262 bid := seatBid.Bid[i] 263 if strings.HasSuffix(bid.ImpID, mf_suffix) { 264 sfxStart := len(bid.ImpID) - len(mf_suffix) - 1 265 bid.ImpID = bid.ImpID[:sfxStart] 266 } 267 bidType, err := getMediaTypeForBid(&bid) 268 if err != nil { 269 return nil, []error{err} 270 } 271 bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ 272 Bid: &bid, 273 BidType: bidType, 274 }) 275 } 276 return bidResponse, nil 277 } 278 279 // getMediaTypeForImp figures out which media type this bid is for 280 func getMediaTypeForBid(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { 281 switch bid.MType { 282 case openrtb2.MarkupBanner: 283 return openrtb_ext.BidTypeBanner, nil 284 case openrtb2.MarkupAudio: 285 return openrtb_ext.BidTypeAudio, nil 286 case openrtb2.MarkupNative: 287 return openrtb_ext.BidTypeNative, nil 288 case openrtb2.MarkupVideo: 289 return openrtb_ext.BidTypeVideo, nil 290 default: 291 return "", &errortypes.BadServerResponse{ 292 Message: fmt.Sprintf("Unsupported MType %d", bid.MType), 293 } 294 } 295 } 296 297 func newBadInputError(message string) error { 298 return &errortypes.BadInput{ 299 Message: message, 300 } 301 } 302 303 func newBadServerResponseError(message string) error { 304 return &errortypes.BadServerResponse{ 305 Message: message, 306 } 307 } 308 309 // Builder builds a new instance of the Adkernel adapter for the given bidder with the given config. 310 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 311 urlTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) 312 if err != nil { 313 return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) 314 } 315 316 bidder := &adkernelAdapter{ 317 EndpointTemplate: urlTemplate, 318 } 319 return bidder, nil 320 }