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