github.com/prebid/prebid-server@v0.275.0/adapters/bidmachine/bidmachine.go (about) 1 package bidmachine 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "path" 9 "strconv" 10 "text/template" 11 12 "github.com/prebid/openrtb/v19/adcom1" 13 "github.com/prebid/openrtb/v19/openrtb2" 14 15 "github.com/prebid/prebid-server/adapters" 16 "github.com/prebid/prebid-server/config" 17 "github.com/prebid/prebid-server/errortypes" 18 "github.com/prebid/prebid-server/macros" 19 "github.com/prebid/prebid-server/openrtb_ext" 20 ) 21 22 type adapter struct { 23 endpoint *template.Template 24 } 25 26 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 27 headers := http.Header{} 28 headers.Add("Content-Type", "application/json") 29 headers.Add("Accept", "application/json") 30 headers.Add("X-Openrtb-Version", "2.5") 31 32 impressions := request.Imp 33 result := make([]*adapters.RequestData, 0, len(impressions)) 34 errs := make([]error, 0, len(impressions)) 35 36 for _, impression := range impressions { 37 if impression.Banner != nil { 38 banner := impression.Banner 39 if banner.W == nil && banner.H == nil { 40 if banner.Format == nil { 41 errs = append(errs, &errortypes.BadInput{ 42 Message: "Impression with id: " + impression.ID + " has following error: Banner width and height is not provided and banner format is missing. At least one is required", 43 }) 44 continue 45 } 46 if len(banner.Format) == 0 { 47 errs = append(errs, &errortypes.BadInput{ 48 Message: "Impression with id: " + impression.ID + " has following error: Banner width and height is not provided and banner format array is empty. At least one is required", 49 }) 50 continue 51 } 52 } 53 54 } 55 56 var bidderExt adapters.ExtImpBidder 57 err := json.Unmarshal(impression.Ext, &bidderExt) 58 if err != nil { 59 errs = append(errs, err) 60 continue 61 } 62 63 var impressionExt openrtb_ext.ExtImpBidmachine 64 err = json.Unmarshal(bidderExt.Bidder, &impressionExt) 65 if err != nil { 66 errs = append(errs, err) 67 continue 68 } 69 url, err := a.buildEndpointURL(impressionExt) 70 if err != nil { 71 errs = append(errs, err) 72 continue 73 } 74 if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory != nil && *bidderExt.Prebid.IsRewardedInventory == 1 { 75 if impression.Banner != nil && !hasRewardedBattr(impression.Banner.BAttr) { 76 bannerCopy := *impression.Banner 77 bannerCopy.BAttr = copyBAttrWithRewardedInventory(bannerCopy.BAttr) 78 impression.Banner = &bannerCopy 79 } 80 if impression.Video != nil && !hasRewardedBattr(impression.Video.BAttr) { 81 videoCopy := *impression.Video 82 videoCopy.BAttr = copyBAttrWithRewardedInventory(videoCopy.BAttr) 83 impression.Video = &videoCopy 84 } 85 } 86 request.Imp = []openrtb2.Imp{impression} 87 body, err := json.Marshal(request) 88 if err != nil { 89 errs = append(errs, err) 90 continue 91 } 92 result = append(result, &adapters.RequestData{ 93 Method: "POST", 94 Uri: url, 95 Body: body, 96 Headers: headers, 97 }) 98 } 99 100 request.Imp = impressions 101 102 return result, errs 103 } 104 105 func hasRewardedBattr(attr []adcom1.CreativeAttribute) bool { 106 for i := 0; i < len(attr); i++ { 107 if attr[i] == adcom1.AttrHasSkipButton { 108 return true 109 } 110 } 111 return false 112 } 113 114 func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { 115 var errs []error 116 117 switch responseData.StatusCode { 118 case http.StatusNoContent: 119 return nil, nil 120 case http.StatusServiceUnavailable: 121 fallthrough 122 case http.StatusBadRequest: 123 fallthrough 124 case http.StatusUnauthorized: 125 fallthrough 126 case http.StatusForbidden: 127 return nil, []error{&errortypes.BadInput{ 128 Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode) + " " + string(responseData.Body), 129 }} 130 case http.StatusOK: 131 break 132 default: 133 return nil, []error{&errortypes.BadServerResponse{ 134 Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode) + " " + string(responseData.Body), 135 }} 136 } 137 138 var bidResponse openrtb2.BidResponse 139 err := json.Unmarshal(responseData.Body, &bidResponse) 140 if err != nil { 141 return nil, []error{&errortypes.BadServerResponse{ 142 Message: err.Error(), 143 }} 144 } 145 146 response := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) 147 148 for _, seatBid := range bidResponse.SeatBid { 149 for _, bid := range seatBid.Bid { 150 thisBid := bid 151 bidType := GetMediaTypeForImp(bid.ImpID, request.Imp) 152 if bidType == UndefinedMediaType { 153 errs = append(errs, &errortypes.BadServerResponse{ 154 Message: "ignoring bid id=" + bid.ID + ", request doesn't contain any valid impression with id=" + bid.ImpID, 155 }) 156 continue 157 } 158 response.Bids = append(response.Bids, &adapters.TypedBid{ 159 Bid: &thisBid, 160 BidType: bidType, 161 }) 162 } 163 } 164 165 return response, errs 166 } 167 168 // Builder builds a new instance of the Bidmachine adapter for the given bidder with the given config. 169 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 170 template, err := template.New("endpointTemplate").Parse(config.Endpoint) 171 if err != nil { 172 return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) 173 } 174 175 bidder := &adapter{ 176 endpoint: template, 177 } 178 179 return bidder, nil 180 } 181 182 const UndefinedMediaType = openrtb_ext.BidType("") 183 184 func (a *adapter) buildEndpointURL(params openrtb_ext.ExtImpBidmachine) (string, error) { 185 endpointParams := macros.EndpointTemplateParams{Host: params.Host} 186 uriString, errMacros := macros.ResolveMacros(a.endpoint, endpointParams) 187 if errMacros != nil { 188 return "", &errortypes.BadInput{ 189 Message: "Failed to resolve host macros", 190 } 191 } 192 uri, errUrl := url.Parse(uriString) 193 if errUrl != nil || uri.Scheme == "" || uri.Host == "" { 194 return "", &errortypes.BadInput{ 195 Message: "Failed to create final URL with provided host", 196 } 197 } 198 uri.Path = path.Join(uri.Path, params.Path) 199 uri.Path = path.Join(uri.Path, params.SellerID) 200 return uri.String(), nil 201 } 202 203 func copyBAttrWithRewardedInventory(src []adcom1.CreativeAttribute) []adcom1.CreativeAttribute { 204 dst := make([]adcom1.CreativeAttribute, len(src)) 205 copy(dst, src) 206 dst = append(dst, adcom1.AttrHasSkipButton) 207 return dst 208 } 209 210 func GetMediaTypeForImp(impID string, imps []openrtb2.Imp) openrtb_ext.BidType { 211 mediaType := openrtb_ext.BidTypeBanner 212 for _, imp := range imps { 213 if imp.ID == impID { 214 if imp.Banner == nil && imp.Video != nil { 215 mediaType = openrtb_ext.BidTypeVideo 216 } 217 return mediaType 218 } 219 } 220 return UndefinedMediaType 221 }