diff --git a/adapters/beintoo/beintoo.go b/adapters/beintoo/beintoo.go new file mode 100644 index 00000000000..fb511f12075 --- /dev/null +++ b/adapters/beintoo/beintoo.go @@ -0,0 +1,222 @@ +package beintoo + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/mxmCherry/openrtb" + "github.com/prebid/prebid-server/adapters" + "github.com/prebid/prebid-server/errortypes" + "github.com/prebid/prebid-server/openrtb_ext" +) + +type BeintooAdapter struct { + endpoint string +} + +func (a *BeintooAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errors []error + + if len(request.Imp) == 0 { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("No Imps in Bid Request"), + }} + } + + if errors := preprocess(request); errors != nil && len(errors) > 0 { + return nil, append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("Error in preprocess of Imp, err: %s", errors), + }) + } + + data, err := json.Marshal(request) + if err != nil { + return nil, []error{&errortypes.BadInput{ + Message: fmt.Sprintf("Error in packaging request to JSON"), + }} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + if request.Device != nil { + addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA) + addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IP) + addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language) + if request.Device.DNT != nil { + addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT))) + } + } + if request.Site != nil { + addHeaderIfNonEmpty(headers, "Referer", request.Site.Page) + } + + return []*adapters.RequestData{{ + Method: "POST", + Uri: a.endpoint, + Body: data, + Headers: headers, + }}, errors +} + +func unpackImpExt(imp *openrtb.Imp) (*openrtb_ext.ExtImpBeintoo, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: err.Error(), + } + } + + var beintooExt openrtb_ext.ExtImpBeintoo + if err := json.Unmarshal(bidderExt.Bidder, &beintooExt); err != nil { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, invalid ImpExt", imp.ID), + } + } + + tagIDValidation, err := strconv.ParseInt(beintooExt.TagID, 10, 64) + if err != nil || tagIDValidation == 0 { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, invalid tagid must be a String of numbers", imp.ID), + } + } + + return &beintooExt, nil +} + +func buildImpBanner(imp *openrtb.Imp) error { + imp.Ext = nil + + if imp.Banner == nil { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Request needs to include a Banner object"), + } + } + + bannerCopy := *imp.Banner + banner := &bannerCopy + + if banner.W == nil && banner.H == nil { + if len(banner.Format) == 0 { + return &errortypes.BadInput{ + Message: fmt.Sprintf("Need at least one size to build request"), + } + } + format := banner.Format[0] + banner.Format = banner.Format[1:] + banner.W = &format.W + banner.H = &format.H + imp.Banner = banner + } + + return nil +} + +// Add Beintoo required properties to Imp object +func addImpProps(imp *openrtb.Imp, secure *int8, BeintooExt *openrtb_ext.ExtImpBeintoo) { + imp.TagID = BeintooExt.TagID + imp.Secure = secure + + if BeintooExt.BidFloor != "" { + bidFloor, err := strconv.ParseFloat(BeintooExt.BidFloor, 64) + if err != nil { + bidFloor = 0 + } + + if bidFloor > 0 { + imp.BidFloor = bidFloor + } + } + + return +} + +// Adding header fields to request header +func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) { + if len(headerValue) > 0 { + headers.Add(headerName, headerValue) + } +} + +// Handle request errors and formatting to be sent to Beintoo +func preprocess(request *openrtb.BidRequest) []error { + errors := make([]error, 0, len(request.Imp)) + resImps := make([]openrtb.Imp, 0, len(request.Imp)) + secure := int8(0) + + if request.Site != nil && request.Site.Page != "" { + pageURL, err := url.Parse(request.Site.Page) + if err == nil && pageURL.Scheme == "https" { + secure = int8(1) + } + } + + for _, imp := range request.Imp { + beintooExt, err := unpackImpExt(&imp) + if err != nil { + errors = append(errors, err) + return errors + } + + addImpProps(&imp, &secure, beintooExt) + + if err := buildImpBanner(&imp); err != nil { + errors = append(errors, err) + return errors + } + resImps = append(resImps, imp) + } + + request.Imp = resImps + + return errors +} + +// MakeBids make the bids for the bid response. +func (a *BeintooAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { + + if response.StatusCode == http.StatusNoContent { + // no bid response + return nil, nil + } + + if response.StatusCode != http.StatusOK { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Invalid Status Returned: %d. Run with request.debug = 1 for more info", response.StatusCode), + }} + } + + var bidResp openrtb.BidResponse + + if err := json.Unmarshal(response.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unable to unpackage bid response. Error: %s", err.Error()), + }} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(1) + + for _, sb := range bidResp.SeatBid { + for i := range sb.Bid { + sb.Bid[i].ImpID = sb.Bid[i].ID + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &sb.Bid[i], + BidType: "banner", + }) + } + } + + return bidResponse, nil + +} + +func NewBeintooBidder(endpoint string) *BeintooAdapter { + return &BeintooAdapter{ + endpoint: endpoint, + } +} diff --git a/adapters/beintoo/beintoo_test.go b/adapters/beintoo/beintoo_test.go new file mode 100644 index 00000000000..863da1513e5 --- /dev/null +++ b/adapters/beintoo/beintoo_test.go @@ -0,0 +1,12 @@ +package beintoo + +import ( + "testing" + + "github.com/prebid/prebid-server/adapters/adapterstest" +) + +func TestJsonSamples(t *testing.T) { + beintooAdapter := NewBeintooBidder("https://ib.beintoo.com") + adapterstest.RunJSONBidderTest(t, "beintootest", beintooAdapter) +} diff --git a/adapters/beintoo/beintootest/exemplary/minimal-banner.json b/adapters/beintoo/beintootest/exemplary/minimal-banner.json new file mode 100644 index 00000000000..60e481c507c --- /dev/null +++ b/adapters/beintoo/beintootest/exemplary/minimal-banner.json @@ -0,0 +1,117 @@ +{ + "mockBidRequest": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "tagid": "25251" + } + } + }], + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "123.123.123.123", + "dnt": 1 + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ib.beintoo.com", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ], + "X-Forwarded-For": [ + "123.123.123.123" + ], + "Referer": [ + "http://www.publisher.com/awesome/site?with=some¶meters=here" + ], + "Dnt": [ + "1" + ], + "User-Agent": [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36" + ] + }, + "body": { + "id": "some_test_auction", + "imp": [{ + "id": "some_test_ad_id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }], + "w": 300, + "h": 250 + }, + "tagid": "25251", + "secure": 0 + }], + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/awesome/site?with=some¶meters=here" + }, + "device": { + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36", + "ip": "123.123.123.123", + "dnt": 1 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "some_test_auction", + "seatbid": [{ + "seat": "12356", + "bid": [{ + "adm": "