forked from prebid/prebid-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
15 changed files
with
1,199 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package alkimi | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"github.com/prebid/prebid-server/v2/errortypes" | ||
"github.com/prebid/prebid-server/v2/floors" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/prebid/openrtb/v19/openrtb2" | ||
"github.com/prebid/prebid-server/v2/adapters" | ||
"github.com/prebid/prebid-server/v2/config" | ||
"github.com/prebid/prebid-server/v2/openrtb_ext" | ||
) | ||
|
||
const price_macro = "${AUCTION_PRICE}" | ||
|
||
type adapter struct { | ||
endpoint string | ||
} | ||
|
||
type extObj struct { | ||
AlkimiBidderExt openrtb_ext.ExtImpAlkimi `json:"bidder"` | ||
} | ||
|
||
// Builder builds a new instance of the Alkimi adapter for the given bidder with the given config. | ||
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { | ||
endpointURL, err := url.Parse(config.Endpoint) | ||
if err != nil || len(endpointURL.String()) == 0 { | ||
return nil, fmt.Errorf("invalid endpoint: %v", err) | ||
} | ||
|
||
bidder := &adapter{ | ||
endpoint: endpointURL.String(), | ||
} | ||
return bidder, nil | ||
} | ||
|
||
// MakeRequests creates Alkimi adapter requests | ||
func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, req *adapters.ExtraRequestInfo) (reqsBidder []*adapters.RequestData, errs []error) { | ||
reqCopy := *request | ||
|
||
updated, errs := updateImps(reqCopy) | ||
if len(errs) > 0 || len(reqCopy.Imp) != len(updated) { | ||
return nil, errs | ||
} | ||
|
||
reqCopy.Imp = updated | ||
encoded, err := json.Marshal(reqCopy) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} else { | ||
reqBidder := buildBidderRequest(adapter, encoded) | ||
reqsBidder = append(reqsBidder, reqBidder) | ||
} | ||
return | ||
} | ||
|
||
func updateImps(bidRequest openrtb2.BidRequest) ([]openrtb2.Imp, []error) { | ||
var errs []error | ||
|
||
updatedImps := make([]openrtb2.Imp, 0, len(bidRequest.Imp)) | ||
for _, imp := range bidRequest.Imp { | ||
|
||
var bidderExt adapters.ExtImpBidder | ||
var extImpAlkimi openrtb_ext.ExtImpAlkimi | ||
|
||
if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { | ||
errs = append(errs, err) | ||
continue | ||
} | ||
|
||
if err := json.Unmarshal(bidderExt.Bidder, &extImpAlkimi); err != nil { | ||
errs = append(errs, err) | ||
continue | ||
} | ||
|
||
var bidFloorPrice floors.Price | ||
bidFloorPrice.FloorMinCur = imp.BidFloorCur | ||
bidFloorPrice.FloorMin = imp.BidFloor | ||
|
||
if len(bidFloorPrice.FloorMinCur) > 0 && bidFloorPrice.FloorMin > 0 { | ||
imp.BidFloor = bidFloorPrice.FloorMin | ||
} else { | ||
imp.BidFloor = extImpAlkimi.BidFloor | ||
} | ||
imp.Instl = extImpAlkimi.Instl | ||
imp.Exp = extImpAlkimi.Exp | ||
|
||
temp := extObj{AlkimiBidderExt: extImpAlkimi} | ||
temp.AlkimiBidderExt.AdUnitCode = imp.ID | ||
|
||
extJson, err := json.Marshal(temp) | ||
if err != nil { | ||
errs = append(errs, err) | ||
continue | ||
} | ||
imp.Ext = extJson | ||
updatedImps = append(updatedImps, imp) | ||
} | ||
|
||
return updatedImps, errs | ||
} | ||
|
||
func buildBidderRequest(adapter *adapter, encoded []byte) *adapters.RequestData { | ||
headers := http.Header{} | ||
headers.Add("Content-Type", "application/json;charset=utf-8") | ||
headers.Add("Accept", "application/json") | ||
|
||
reqBidder := &adapters.RequestData{ | ||
Method: "POST", | ||
Uri: adapter.endpoint, | ||
Body: encoded, | ||
Headers: headers, | ||
} | ||
return reqBidder | ||
} | ||
|
||
// MakeBids will parse the bids from the Alkimi server | ||
func (adapter *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { | ||
var errs []error | ||
|
||
if adapters.IsResponseStatusCodeNoContent(response) { | ||
return nil, nil | ||
} | ||
|
||
if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
var bidResp openrtb2.BidResponse | ||
err := json.Unmarshal(response.Body, &bidResp) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
seatBidCount := len(bidResp.SeatBid) | ||
if seatBidCount == 0 { | ||
return nil, []error{&errortypes.BadServerResponse{ | ||
Message: "Empty SeatBid array", | ||
}} | ||
} | ||
|
||
bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) | ||
for _, seatBid := range bidResp.SeatBid { | ||
for _, bid := range seatBid.Bid { | ||
copyBid := bid | ||
resolveMacros(©Bid) | ||
impId := copyBid.ImpID | ||
imp := request.Imp | ||
bidType, err := getMediaTypeForImp(impId, imp) | ||
if err != nil { | ||
errs = append(errs, err) | ||
continue | ||
} | ||
bidderBid := &adapters.TypedBid{ | ||
Bid: ©Bid, | ||
BidType: bidType, | ||
} | ||
bidResponse.Bids = append(bidResponse.Bids, bidderBid) | ||
} | ||
} | ||
return bidResponse, errs | ||
} | ||
|
||
func resolveMacros(bid *openrtb2.Bid) { | ||
strPrice := strconv.FormatFloat(bid.Price, 'f', -1, 64) | ||
bid.NURL = strings.Replace(bid.NURL, price_macro, strPrice, -1) | ||
bid.AdM = strings.Replace(bid.AdM, price_macro, strPrice, -1) | ||
} | ||
|
||
func getMediaTypeForImp(impId string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { | ||
for _, imp := range imps { | ||
if imp.ID == impId { | ||
if imp.Banner != nil { | ||
return openrtb_ext.BidTypeBanner, nil | ||
} | ||
if imp.Video != nil { | ||
return openrtb_ext.BidTypeVideo, nil | ||
} | ||
if imp.Audio != nil { | ||
return openrtb_ext.BidTypeAudio, nil | ||
} | ||
} | ||
} | ||
return "", &errortypes.BadInput{ | ||
Message: fmt.Sprintf("Failed to find imp \"%s\"", impId), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package alkimi | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/prebid/prebid-server/v2/adapters" | ||
"github.com/prebid/prebid-server/v2/adapters/adapterstest" | ||
"github.com/prebid/prebid-server/v2/config" | ||
"github.com/prebid/prebid-server/v2/openrtb_ext" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
const ( | ||
alkimiTestEndpoint = "https://exchange.alkimi-onboarding.com/server/bid" | ||
) | ||
|
||
func TestJsonSamples(t *testing.T) { | ||
bidder, buildErr := Builder( | ||
openrtb_ext.BidderAlkimi, | ||
config.Adapter{Endpoint: alkimiTestEndpoint}, | ||
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, | ||
) | ||
|
||
if buildErr != nil { | ||
t.Fatalf("Builder returned unexpected error %v", buildErr) | ||
} | ||
|
||
adapterstest.RunJSONBidderTest(t, "alkimitest", bidder) | ||
} | ||
|
||
func TestEndpointEmpty(t *testing.T) { | ||
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{ | ||
Endpoint: ""}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"}) | ||
assert.Error(t, buildErr) | ||
} | ||
|
||
func TestEndpointMalformed(t *testing.T) { | ||
_, buildErr := Builder(openrtb_ext.BidderAlkimi, config.Adapter{ | ||
Endpoint: " http://leading.space.is.invalid"}, config.Server{ExternalUrl: alkimiTestEndpoint, GvlID: 1, DataCenter: "2"}) | ||
assert.Error(t, buildErr) | ||
} | ||
|
||
func TestBuilder(t *testing.T) { | ||
bidder, buildErr := buildBidder() | ||
if buildErr != nil { | ||
t.Fatalf("Failed to build bidder: %v", buildErr) | ||
} | ||
assert.NotNil(t, bidder) | ||
} | ||
|
||
func buildBidder() (adapters.Bidder, error) { | ||
return Builder( | ||
openrtb_ext.BidderAlkimi, | ||
config.Adapter{Endpoint: alkimiTestEndpoint}, | ||
config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
{ | ||
"mockBidRequest": { | ||
"id": "test-request-id", | ||
"device": { | ||
"ip": "123.123.123.123", | ||
"ua": "iPad" | ||
}, | ||
"site": { | ||
"domain": "www.example.com", | ||
"page": "http://www.example.com", | ||
"publisher": { | ||
"domain": "example.com" | ||
}, | ||
"ext": { | ||
"amp": 0 | ||
} | ||
}, | ||
"imp": [ | ||
{ | ||
"id": "test-imp-id", | ||
"tagid": "test", | ||
"audio": { | ||
"mimes": [ | ||
"audio/mpeg", | ||
"audio/mp3" | ||
], | ||
"minduration": 5, | ||
"maxduration": 30, | ||
"minbitrate": 32, | ||
"maxbitrate": 128 | ||
}, | ||
"bidfloor": 0.7, | ||
"bidfloorcur": "USD", | ||
"ext": { | ||
"bidder": { | ||
"token": "XXX", | ||
"bidFloor": 0.5 | ||
} | ||
} | ||
} | ||
] | ||
}, | ||
"httpCalls": [ | ||
{ | ||
"expectedRequest": { | ||
"uri": "https://exchange.alkimi-onboarding.com/server/bid", | ||
"body": { | ||
"id": "test-request-id", | ||
"imp": [ | ||
{ | ||
"id": "test-imp-id", | ||
"tagid": "test", | ||
"audio": { | ||
"mimes": [ | ||
"audio/mpeg", | ||
"audio/mp3" | ||
], | ||
"minduration": 5, | ||
"maxduration": 30, | ||
"minbitrate": 32, | ||
"maxbitrate": 128 | ||
}, | ||
"bidfloor": 0.7, | ||
"bidfloorcur": "USD", | ||
"ext": { | ||
"bidder": { | ||
"token": "XXX", | ||
"bidFloor": 0.5, | ||
"adUnitCode": "test-imp-id", | ||
"exp": 0, | ||
"instl": 0 | ||
} | ||
} | ||
} | ||
], | ||
"site": { | ||
"domain": "www.example.com", | ||
"page": "http://www.example.com", | ||
"publisher": { | ||
"domain": "example.com" | ||
}, | ||
"ext": { | ||
"amp": 0 | ||
} | ||
}, | ||
"device": { | ||
"ip": "123.123.123.123", | ||
"ua": "iPad" | ||
} | ||
} | ||
}, | ||
"mockResponse": { | ||
"status": 200, | ||
"body": { | ||
"id": "test-request-id", | ||
"seatbid": [ | ||
{ | ||
"bid": [ | ||
{ | ||
"id": "test_bid_id", | ||
"impid": "test-imp-id", | ||
"price": 0.9, | ||
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>", | ||
"cid": "test_cid", | ||
"crid": "test_crid", | ||
"ext": { | ||
"prebid": { | ||
"type": "audio" | ||
} | ||
} | ||
} | ||
], | ||
"seat": "alkimi" | ||
} | ||
], | ||
"cur": "USD" | ||
} | ||
} | ||
} | ||
], | ||
"expectedBidResponses": [ | ||
{ | ||
"bids": [ | ||
{ | ||
"bid": { | ||
"id": "test_bid_id", | ||
"impid": "test-imp-id", | ||
"price": 0.9, | ||
"adm": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><VAST version=\"2.0\"><Ad id=\"128a6.44d74.46b3\"><InLine><Error><![CDATA[http:\/\/example.net\/hbx\/verr?e=]]><\/Error><Impression><![CDATA[http:\/\/example.net\/hbx\/vimp?lid=test&aid=testapp]]><\/Impression><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15<\/Duration><TrackingEvents><Tracking event=\"firstQuartile\"><![CDATA[https:\/\/example.com?event=first_quartile]]><\/Tracking><\/TrackingEvents><VideoClicks><ClickThrough><![CDATA[http:\/\/example.com]]><\/ClickThrough><\/VideoClicks><MediaFiles><MediaFile delivery=\"progressive\" width=\"16\" height=\"9\" type=\"audio\/mp3\" bitrate=\"128\"><![CDATA[https:\/\/example.com\/media.mp4]]><\/MediaFile><\/MediaFiles><\/Linear><\/Creative><\/Creatives><\/InLine><\/Ad><\/VAST>", | ||
"cid": "test_cid", | ||
"crid": "test_crid", | ||
"ext": { | ||
"prebid": { | ||
"type": "audio" | ||
} | ||
} | ||
}, | ||
"type": "audio" | ||
} | ||
] | ||
} | ||
] | ||
} |
Oops, something went wrong.