Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions adapters/matterfullrtb/matterfullrtb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
package matterfullrtb

import (
"fmt"
"net/http"
"net/url"
"text/template"

"github.com/prebid/openrtb/v20/openrtb2"
"github.com/prebid/prebid-server/v4/adapters"
"github.com/prebid/prebid-server/v4/config"
"github.com/prebid/prebid-server/v4/errortypes"
"github.com/prebid/prebid-server/v4/macros"
"github.com/prebid/prebid-server/v4/openrtb_ext"
"github.com/prebid/prebid-server/v4/util/iterutil"
"github.com/prebid/prebid-server/v4/util/jsonutil"
)

type adapter struct {
EndpointTemplate *template.Template
}

// MakeRequests prepares request information for prebid-server core
func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errs []error

pub2impressions, impErrs := getImpressionsInfo(request.Imp)
errs = append(errs, impErrs...)
if len(pub2impressions) == 0 {
return nil, errs
}

result := make([]*adapters.RequestData, 0, len(pub2impressions))
for k, imps := range pub2impressions {
bidRequest, err := a.buildAdapterRequest(request, &k, imps)
if err != nil {
errs = append(errs, err)
continue
}
result = append(result, bidRequest)
}
return result, errs
}

// getImpressionsInfo checks each impression for validity and returns valid impressions grouped by ext params
func getImpressionsInfo(imps []openrtb2.Imp) (map[openrtb_ext.ExtImpMatterfullRTB][]openrtb2.Imp, []error) {
var errors []error
res := make(map[openrtb_ext.ExtImpMatterfullRTB][]openrtb2.Imp)

for i := range imps {
imp := imps[i] // value copy so compatImpression does not mutate the original request
impExt, err := getImpressionExt(&imp)
if err != nil {
errors = append(errors, err)
continue
}
if err := compatImpression(&imp); err != nil {
errors = append(errors, err)
continue
}
res[impExt] = append(res[impExt], imp)
}
return res, errors
}
Comment thread
Matterfull marked this conversation as resolved.

// Alter impression info to comply with Matterfull RTB requirements
func compatImpression(imp *openrtb2.Imp) error {
imp.Ext = nil // do not forward ext to Matterfull RTB
if imp.Banner != nil {
return compatBannerImpression(imp)
}
return nil
}

func compatBannerImpression(imp *openrtb2.Imp) error {
if imp.Banner.W == nil || imp.Banner.H == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (non-blocking): when exactly one of w/h is explicitly set (legal per OpenRTB), this branch discards the explicit value and overwrites both from format[0]. Intentional platform normalization, or should only the missing dimension be filled in?

This mirrors lunamedia verbatim, so I suspect it's deliberate — but no test covers the "one of w/h present + format present" case, so the intent isn't pinned anywhere. A one-line comment or a TestCompatBannerImpression subtest for the mixed case would remove the ambiguity.

bannerCopy := *imp.Banner
banner := &bannerCopy

if len(banner.Format) == 0 {
return &errortypes.BadInput{
Message: "Expected at least one banner.format entry or explicit w/h",
}
}

format := banner.Format[0]

// Deep copy the remaining formats
if len(banner.Format) > 1 {
newFormats := make([]openrtb2.Format, len(banner.Format)-1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): this "keep the remaining formats" branch is only covered by the Go unit test — no JSON fixture exercises it through the wire. Every fixture with a multi-entry format also sets top-level w/h (so this path never fires), while compat.json and single-format.json both use a single-entry format (hitting the Format = nil branch — they're duplicates of each other, incidentally).

Consider repurposing one of them to format: [{a},{b}] with no top-level w/h, asserting the request body carries w/h from the first entry and format: [{b}].

copy(newFormats, banner.Format[1:])
banner.Format = newFormats
} else {
banner.Format = nil
}

banner.W = &format.W
banner.H = &format.H

imp.Banner = banner
}
return nil
}

func getImpressionExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpMatterfullRTB, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(imp.Ext, &bidderExt); err != nil {
return openrtb_ext.ExtImpMatterfullRTB{}, &errortypes.BadInput{
Message: err.Error(),
}
}
var matterfullExt openrtb_ext.ExtImpMatterfullRTB
if err := jsonutil.Unmarshal(bidderExt.Bidder, &matterfullExt); err != nil {
return openrtb_ext.ExtImpMatterfullRTB{}, &errortypes.BadInput{
Message: err.Error(),
}
}

return matterfullExt, nil
}

func (a *adapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpMatterfullRTB, imps []openrtb2.Imp) (*adapters.RequestData, error) {
newBidRequest := createBidRequest(prebidBidRequest, imps)
reqJSON, err := jsonutil.Marshal(newBidRequest)
if err != nil {
return nil, err
}

headers := http.Header{}
headers.Add("Content-Type", "application/json;charset=utf-8")
headers.Add("Accept", "application/json")
headers.Add("x-openrtb-version", "2.6")

url, err := a.buildEndpointURL(params)
if err != nil {
return nil, err
}

return &adapters.RequestData{
Method: "POST",
Uri: url,
Body: reqJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(imps)}, nil
}

func createBidRequest(prebidBidRequest *openrtb2.BidRequest, imps []openrtb2.Imp) *openrtb2.BidRequest {

reqCopy := *prebidBidRequest
newBidRequest := &reqCopy
newBidRequest.Imp = imps

if newBidRequest.Site != nil {
siteCopy := *newBidRequest.Site
newBidRequest.Site = &siteCopy
newBidRequest.Site.Publisher = nil
newBidRequest.Site.Domain = ""
}

if newBidRequest.App != nil {
appCopy := *newBidRequest.App
newBidRequest.App = &appCopy
newBidRequest.App.Publisher = nil
}

return newBidRequest
}

// Builds endpoint url based on adapter-specific pub settings from imp.ext
func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtImpMatterfullRTB) (string, error) {
endpointParams := macros.EndpointTemplateParams{PublisherID: url.QueryEscape(params.PublisherID)}
return macros.ResolveMacros(a.EndpointTemplate, endpointParams)
}

// MakeBids translates Matterfull RTB bid response to prebid-server specific format
func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if adapters.IsResponseStatusCodeNoContent(response) {
return nil, nil
}

if err := adapters.CheckResponseStatusCodeForErrors(response); err != nil {
return nil, []error{err}
}

var bidResp openrtb2.BidResponse
if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil {
msg := fmt.Sprintf("Bad server response: %v", err)
return nil, []error{&errortypes.BadServerResponse{Message: msg}}
}

if len(bidResp.SeatBid) == 0 {
return nil, nil
}

bidResponse := adapters.NewBidderResponse()
if bidResp.Cur != "" {
bidResponse.Currency = bidResp.Cur
}

var errs []error
for _, seatBid := range bidResp.SeatBid {
for bid := range iterutil.SlicePointerValues(seatBid.Bid) {
bidType, err := getMediaTypeForBid(bid, internalRequest.Imp)
if err != nil {
errs = append(errs, err)
continue
}
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: bid,
BidType: bidType,
})
}
}

return bidResponse, errs
Comment thread
postindustria-code marked this conversation as resolved.
}

func getMediaTypeForBid(bid *openrtb2.Bid, imps []openrtb2.Imp) (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.MarkupNative:
return openrtb_ext.BidTypeNative, nil
case openrtb2.MarkupAudio:
return openrtb_ext.BidTypeAudio, nil
case 0:
return getMediaTypeForImpID(bid.ImpID, imps)
default:
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Unsupported mtype %d for impression with ID: %s", bid.MType, bid.ImpID),
}
}
}

func getMediaTypeForImpID(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
for imp := range iterutil.SlicePointerValues(imps) {
if imp.ID == impID {
bidType, found, multiFormat := getMediaTypeForImp(imp)
if multiFormat {
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Bid must have non-zero mtype for multi-format impression with ID: %s", impID),
}
}
if found {
return bidType, nil
}
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Could not determine media type for impression with ID: %s", impID),
}
}
}
return "", &errortypes.BadServerResponse{
Message: fmt.Sprintf("Failed to find impression for ID: %s", impID),
}
}

func getMediaTypeForImp(imp *openrtb2.Imp) (openrtb_ext.BidType, bool, bool) {
var bidType openrtb_ext.BidType
formatCount := 0

if imp.Banner != nil {
bidType = openrtb_ext.BidTypeBanner
formatCount++
}
if imp.Video != nil {
bidType = openrtb_ext.BidTypeVideo
formatCount++
}
if imp.Audio != nil {
bidType = openrtb_ext.BidTypeAudio
formatCount++
}
if imp.Native != nil {
bidType = openrtb_ext.BidTypeNative
formatCount++
}

return bidType, formatCount == 1, formatCount > 1
}

// Builder builds a new instance of the Matterfull RTB adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
urlTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse endpoint url template: %w", err)
}

bidder := &adapter{
EndpointTemplate: urlTemplate,
}
return bidder, nil
}
Loading
Loading