diff --git a/adapters/concert/concert.go b/adapters/concert/concert.go new file mode 100644 index 00000000000..26bb105842a --- /dev/null +++ b/adapters/concert/concert.go @@ -0,0 +1,139 @@ +package concert + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +const adapterVersion = "1.0.0" + +// Builder builds a new instance of the Concert adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + bidderImpExt, err := getBidderExt(request.Imp[0]) + if err != nil { + return nil, []error{fmt.Errorf("get bidder ext: %v", err)} + } + + requestJSON, err := json.Marshal(request) + if err != nil { + return nil, []error{err} + } + + var requestMap map[string]interface{} + err = json.Unmarshal(requestJSON, &requestMap) + if err != nil { + return nil, []error{err} + } + + if requestMap["ext"] == nil { + requestMap["ext"] = make(map[string]interface{}) + } + requestMap["ext"].(map[string]interface{})["adapterVersion"] = adapterVersion + requestMap["ext"].(map[string]interface{})["partnerId"] = bidderImpExt.PartnerId + + requestJSON, err = json.Marshal(requestMap) + if err != nil { + return nil, []error{err} + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json") + headers.Add("Accept", "application/json") + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + } + + return []*adapters.RequestData{requestData}, nil +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errors []error + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + errors = append(errors, err) + continue + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + if len(errors) > 0 { + return nil, errors + } + + if len(bidResponse.Bids) == 0 { + return nil, []error{fmt.Errorf("no bids returned")} + } + + return bidResponse, nil +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return "", fmt.Errorf("native media types are not yet supported") + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse media type for bid: \"%s\"", bid.ImpID), + } + } +} + +func getBidderExt(imp openrtb2.Imp) (bidderImpExt openrtb_ext.ImpExtConcert, err error) { + var impExt adapters.ExtImpBidder + if err = json.Unmarshal(imp.Ext, &impExt); err != nil { + return bidderImpExt, fmt.Errorf("imp ext: %v", err) + } + if err = json.Unmarshal(impExt.Bidder, &bidderImpExt); err != nil { + return bidderImpExt, fmt.Errorf("bidder ext: %v", err) + } + return bidderImpExt, nil +} diff --git a/adapters/concert/concert_test.go b/adapters/concert/concert_test.go new file mode 100644 index 00000000000..e8c06d06377 --- /dev/null +++ b/adapters/concert/concert_test.go @@ -0,0 +1,20 @@ +package concert + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderConcert, config.Adapter{ + Endpoint: "https://bids.concert.io/bids/openrtb"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "concerttest", bidder) +} diff --git a/adapters/concert/concerttest/exemplary/audio.json b/adapters/concert/concerttest/exemplary/audio.json new file mode 100644 index 00000000000..66363d5a256 --- /dev/null +++ b/adapters/concert/concerttest/exemplary/audio.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp3"], + "minduration": 5, + "maxduration": 30, + "protocols": [1, 2] + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name" + } + } + } + ], + "site": { + "page": "http://www.example.com" + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bids.concert.io/bids/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp3"], + "minduration": 5, + "maxduration": 30, + "protocols": [1, 2] + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name" + } + } + } + ], + "device": { + "ip": "123.123.123.123", + "ua": "test-user-agent" + }, + "site": { + "page": "http://www.example.com" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "...", + "crid": "test-creative-id", + "mtype": 3 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "...", + "crid": "test-creative-id", + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] + } \ No newline at end of file diff --git a/adapters/concert/concerttest/exemplary/banner.json b/adapters/concert/concerttest/exemplary/banner.json new file mode 100644 index 00000000000..6fc0df4e604 --- /dev/null +++ b/adapters/concert/concerttest/exemplary/banner.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 1030, + "h": 590 + } + ] + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name", + "sizes": [ + [ + 1030, + 590 + ] + ] + } + } + } + ], + "site": { + "page": "http://www.example.com" + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bids.concert.io/bids/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 1030, + "h": 590 + } + ] + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name", + "sizes": [ + [ + 1030, + 590 + ] + ] + } + } + } + ], + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123" + }, + "site": { + "page": "http://www.example.com" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "

Banner Ad

This is a mock banner ad

", + "crid": "test-creative-id", + "w": 1030, + "h": 590, + "mtype": 1 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "

Banner Ad

This is a mock banner ad

", + "crid": "test-creative-id", + "w": 1030, + "h": 590, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] + } \ No newline at end of file diff --git a/adapters/concert/concerttest/exemplary/video.json b/adapters/concert/concerttest/exemplary/video.json new file mode 100644 index 00000000000..fdb21d04d4a --- /dev/null +++ b/adapters/concert/concerttest/exemplary/video.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": ["video/mp4"], + "minduration": 5, + "maxduration": 30, + "protocols": [1, 2], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name", + "sizes": [[640, 480]] + } + } + } + ], + "site": { + "page": "http://www.example.com" + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://bids.concert.io/bids/openrtb", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 640, + "h": 480, + "minduration": 5, + "maxduration": 30, + "protocols": [1, 2], + "mimes": ["video/mp4"] + }, + "ext": { + "bidder": { + "partnerId": "partner_name", + "placementId": 1234567, + "site": "site_name", + "slot": "slot_name", + "sizes": [[640, 480]] + } + } + } + ], + "site": { + "page": "http://www.example.com" + }, + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123" + }, + "user": { + "buyeruid": "some-buyer-uid" + }, + "ext": { + "adapterVersion": "1.0.0", + "partnerId": "partner_name" + } + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-response-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "...", + "crid": "test-creative-id", + "w": 640, + "h": 480, + "mtype": 2 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.23, + "adm": "...", + "crid": "test-creative-id", + "w": 640, + "h": 480, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/concert/params_test.go b/adapters/concert/params_test.go new file mode 100644 index 00000000000..4cf653d2b27 --- /dev/null +++ b/adapters/concert/params_test.go @@ -0,0 +1,51 @@ +package concert + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range validParams { + if err := validator.Validate(openrtb_ext.BidderConcert, json.RawMessage(p)); err != nil { + t.Errorf("Schema rejected valid params: %s", p) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json schema. %v", err) + } + + for _, p := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderConcert, json.RawMessage(p)); err == nil { + t.Errorf("Schema allowed invalid params: %s", p) + } + } +} + +var validParams = []string{ + `{"partnerId": "partner_name"}`, + `{"partnerId": "partner_name", "placementId": 1234567}`, + `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name"}`, + `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name", "slot": "slot_name"}`, + `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name", "slot": "slot_name", "sizes": [[1030, 590]]}`, +} + +var invalidParams = []string{ + `{"partnerId": ""}`, + `{"placementId": 1234567}`, + `{"site": "site_name"}`, + `{"slot": "slot_name"}`, + `{"sizes": [[1030, 590]]}`, + `{"placementId": 1234567, "site": "site_name", "slot": "slot_name", "sizes": [[1030, 590]]}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 9f96cdbf171..922fecfa7ef 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -66,6 +66,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/coinzilla" "github.com/prebid/prebid-server/v2/adapters/colossus" "github.com/prebid/prebid-server/v2/adapters/compass" + "github.com/prebid/prebid-server/v2/adapters/concert" "github.com/prebid/prebid-server/v2/adapters/connectad" "github.com/prebid/prebid-server/v2/adapters/consumable" "github.com/prebid/prebid-server/v2/adapters/conversant" @@ -274,6 +275,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderCoinzilla: coinzilla.Builder, openrtb_ext.BidderColossus: colossus.Builder, openrtb_ext.BidderCompass: compass.Builder, + openrtb_ext.BidderConcert: concert.Builder, openrtb_ext.BidderConnectAd: connectad.Builder, openrtb_ext.BidderConsumable: consumable.Builder, openrtb_ext.BidderConversant: conversant.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 93c6a49e8e4..3b3bcb64201 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -82,6 +82,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderCoinzilla, BidderColossus, BidderCompass, + BidderConcert, BidderConnectAd, BidderConsumable, BidderConversant, @@ -368,6 +369,7 @@ const ( BidderCoinzilla BidderName = "coinzilla" BidderColossus BidderName = "colossus" BidderCompass BidderName = "compass" + BidderConcert BidderName = "concert" BidderConnectAd BidderName = "connectad" BidderConsumable BidderName = "consumable" BidderConversant BidderName = "conversant" diff --git a/openrtb_ext/imp_concert.go b/openrtb_ext/imp_concert.go new file mode 100644 index 00000000000..bd5ec245c86 --- /dev/null +++ b/openrtb_ext/imp_concert.go @@ -0,0 +1,9 @@ +package openrtb_ext + +type ImpExtConcert struct { + PartnerId string `json:"partnerId"` + PlacementId *int `json:"placementId,omitempty"` + Site *string `json:"site,omitempty"` + Slot *string `json:"slot,omitempty"` + Sizes *[][]int `json:"sizes,omitempty"` +} diff --git a/static/bidder-info/concert.yaml b/static/bidder-info/concert.yaml new file mode 100644 index 00000000000..8251518f29a --- /dev/null +++ b/static/bidder-info/concert.yaml @@ -0,0 +1,18 @@ +endpoint: "https://bids.concert.io/bids/openrtb" +endpointCompression: gzip +geoscope: + - global +maintainer: + email: support@concert.io +modifyingVastXmlAllowed: true +capabilities: + app: + mediaTypes: + - banner + - video + - audio + site: + mediaTypes: + - banner + - video + - audio \ No newline at end of file diff --git a/static/bidder-params/concert.json b/static/bidder-params/concert.json new file mode 100644 index 00000000000..5dd82b4d486 --- /dev/null +++ b/static/bidder-params/concert.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Concert Adapter Params", + "description": "A schema which validates params accepted by the Concert adapter", + "type": "object", + "properties": { + "partnerId": { + "type": "string", + "description": "The partner id assigned by concert.", + "minLength": 1 + }, + "placementId": { + "type": "integer", + "description": "The placement id." + }, + "site": { + "type": "string", + "description": "The site name." + }, + "slot": { + "type": "string", + "description": "The slot name." + }, + "sizes": { + "type": "array", + "description": "All sizes this ad unit accepts.", + "items": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 2, + "maxItems": 2 + } + } + }, + "required": ["partnerId"] +} \ No newline at end of file