github.com/prebid/prebid-server@v0.275.0/adapters/adkernel/adkernel.go (about)

     1  package adkernel
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"text/template"
     9  
    10  	"github.com/prebid/openrtb/v19/openrtb2"
    11  	"github.com/prebid/prebid-server/adapters"
    12  	"github.com/prebid/prebid-server/config"
    13  	"github.com/prebid/prebid-server/errortypes"
    14  	"github.com/prebid/prebid-server/macros"
    15  	"github.com/prebid/prebid-server/openrtb_ext"
    16  )
    17  
    18  type adkernelAdapter struct {
    19  	EndpointTemplate *template.Template
    20  }
    21  
    22  // MakeRequests prepares request information for prebid-server core
    23  func (adapter *adkernelAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    24  	errs := make([]error, 0, len(request.Imp))
    25  	if len(request.Imp) == 0 {
    26  		errs = append(errs, newBadInputError("No impression in the bid request"))
    27  		return nil, errs
    28  	}
    29  	imps, impExts, err := getImpressionsInfo(request.Imp)
    30  	if len(imps) == 0 {
    31  		return nil, err
    32  	}
    33  	errs = append(errs, err...)
    34  
    35  	pub2impressions, dispErrors := dispatchImpressions(imps, impExts)
    36  	if len(dispErrors) > 0 {
    37  		errs = append(errs, dispErrors...)
    38  	}
    39  	if len(pub2impressions) == 0 {
    40  		return nil, errs
    41  	}
    42  	result := make([]*adapters.RequestData, 0, len(pub2impressions))
    43  	for k, imps := range pub2impressions {
    44  		bidRequest, err := adapter.buildAdapterRequest(request, &k, imps)
    45  		if err != nil {
    46  			errs = append(errs, err)
    47  		} else {
    48  			result = append(result, bidRequest)
    49  		}
    50  	}
    51  	return result, errs
    52  }
    53  
    54  // getImpressionsInfo checks each impression for validity and returns impressions copy with corresponding exts
    55  func getImpressionsInfo(imps []openrtb2.Imp) ([]openrtb2.Imp, []openrtb_ext.ExtImpAdkernel, []error) {
    56  	impsCount := len(imps)
    57  	errors := make([]error, 0, impsCount)
    58  	resImps := make([]openrtb2.Imp, 0, impsCount)
    59  	resImpExts := make([]openrtb_ext.ExtImpAdkernel, 0, impsCount)
    60  
    61  	for _, imp := range imps {
    62  		impExt, err := getImpressionExt(&imp)
    63  		if err != nil {
    64  			errors = append(errors, err)
    65  			continue
    66  		}
    67  		if err := validateImpression(&imp, impExt); err != nil {
    68  			errors = append(errors, err)
    69  			continue
    70  		}
    71  		resImps = append(resImps, imp)
    72  		resImpExts = append(resImpExts, *impExt)
    73  	}
    74  	return resImps, resImpExts, errors
    75  }
    76  
    77  func validateImpression(imp *openrtb2.Imp, impExt *openrtb_ext.ExtImpAdkernel) error {
    78  	if impExt.ZoneId < 1 {
    79  		return newBadInputError(fmt.Sprintf("Invalid zoneId value: %d. Ignoring imp id=%s", impExt.ZoneId, imp.ID))
    80  	}
    81  	if imp.Video == nil && imp.Banner == nil && imp.Native == nil {
    82  		return newBadInputError(fmt.Sprintf("Invalid imp id=%s. Expected imp.banner / imp.video / imp.native", imp.ID))
    83  	}
    84  	return nil
    85  }
    86  
    87  // Group impressions by AdKernel-specific parameter `zoneId`
    88  func dispatchImpressions(imps []openrtb2.Imp, impsExt []openrtb_ext.ExtImpAdkernel) (map[openrtb_ext.ExtImpAdkernel][]openrtb2.Imp, []error) {
    89  	res := make(map[openrtb_ext.ExtImpAdkernel][]openrtb2.Imp)
    90  	errors := make([]error, 0)
    91  	for idx := range imps {
    92  		imp := imps[idx]
    93  		err := compatImpression(&imp)
    94  		if err != nil {
    95  			errors = append(errors, err)
    96  			continue
    97  		}
    98  		impExt := impsExt[idx]
    99  		if res[impExt] == nil {
   100  			res[impExt] = make([]openrtb2.Imp, 0)
   101  		}
   102  		res[impExt] = append(res[impExt], imp)
   103  	}
   104  	return res, errors
   105  }
   106  
   107  // Alter impression info to comply with adkernel platform requirements
   108  func compatImpression(imp *openrtb2.Imp) error {
   109  	imp.Ext = nil //do not forward ext to adkernel platform
   110  	if imp.Banner != nil {
   111  		return compatBannerImpression(imp)
   112  	}
   113  	if imp.Video != nil {
   114  		return compatVideoImpression(imp)
   115  	}
   116  	if imp.Native != nil {
   117  		return compatNativeImpression(imp)
   118  	}
   119  	return newBadInputError("Invalid impression")
   120  }
   121  
   122  func compatBannerImpression(imp *openrtb2.Imp) error {
   123  	imp.Audio = nil
   124  	imp.Video = nil
   125  	imp.Native = nil
   126  	return nil
   127  }
   128  
   129  func compatVideoImpression(imp *openrtb2.Imp) error {
   130  	imp.Banner = nil
   131  	imp.Audio = nil
   132  	imp.Native = nil
   133  	return nil
   134  }
   135  
   136  func compatNativeImpression(imp *openrtb2.Imp) error {
   137  	imp.Banner = nil
   138  	imp.Audio = nil
   139  	imp.Video = nil
   140  	return nil
   141  }
   142  
   143  func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpAdkernel, error) {
   144  	var bidderExt adapters.ExtImpBidder
   145  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   146  		return nil, &errortypes.BadInput{
   147  			Message: err.Error(),
   148  		}
   149  	}
   150  	var adkernelExt openrtb_ext.ExtImpAdkernel
   151  	if err := json.Unmarshal(bidderExt.Bidder, &adkernelExt); err != nil {
   152  		return nil, &errortypes.BadInput{
   153  			Message: err.Error(),
   154  		}
   155  	}
   156  	return &adkernelExt, nil
   157  }
   158  
   159  func (adapter *adkernelAdapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpAdkernel, imps []openrtb2.Imp) (*adapters.RequestData, error) {
   160  	newBidRequest := createBidRequest(prebidBidRequest, params, imps)
   161  	reqJSON, err := json.Marshal(newBidRequest)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	headers := http.Header{}
   167  	headers.Add("Content-Type", "application/json;charset=utf-8")
   168  	headers.Add("Accept", "application/json")
   169  	headers.Add("x-openrtb-version", "2.5")
   170  
   171  	url, err := adapter.buildEndpointURL(params)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	return &adapters.RequestData{
   177  		Method:  "POST",
   178  		Uri:     url,
   179  		Body:    reqJSON,
   180  		Headers: headers}, nil
   181  }
   182  
   183  func createBidRequest(prebidBidRequest *openrtb2.BidRequest, params *openrtb_ext.ExtImpAdkernel, imps []openrtb2.Imp) *openrtb2.BidRequest {
   184  	bidRequest := *prebidBidRequest
   185  	bidRequest.Imp = imps
   186  	if bidRequest.Site != nil {
   187  		// Need to copy Site as Request is a shallow copy
   188  		siteCopy := *bidRequest.Site
   189  		bidRequest.Site = &siteCopy
   190  		bidRequest.Site.Publisher = nil
   191  	}
   192  	if bidRequest.App != nil {
   193  		// Need to copy App as Request is a shallow copy
   194  		appCopy := *bidRequest.App
   195  		bidRequest.App = &appCopy
   196  		bidRequest.App.Publisher = nil
   197  	}
   198  	return &bidRequest
   199  }
   200  
   201  // Builds endpoint url based on adapter-specific pub settings from imp.ext
   202  func (adapter *adkernelAdapter) buildEndpointURL(params *openrtb_ext.ExtImpAdkernel) (string, error) {
   203  	endpointParams := macros.EndpointTemplateParams{ZoneID: strconv.Itoa(params.ZoneId)}
   204  	return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams)
   205  }
   206  
   207  // MakeBids translates adkernel bid response to prebid-server specific format
   208  func (adapter *adkernelAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   209  	if response.StatusCode == http.StatusNoContent {
   210  		return nil, nil
   211  	}
   212  	if response.StatusCode != http.StatusOK {
   213  		return nil, []error{
   214  			newBadServerResponseError(fmt.Sprintf("Unexpected http status code: %d", response.StatusCode)),
   215  		}
   216  	}
   217  	var bidResp openrtb2.BidResponse
   218  	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
   219  		return nil, []error{
   220  			newBadServerResponseError(fmt.Sprintf("Bad server response: %d", err)),
   221  		}
   222  	}
   223  
   224  	if len(bidResp.SeatBid) != 1 {
   225  		return nil, []error{
   226  			newBadServerResponseError(fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid))),
   227  		}
   228  	}
   229  
   230  	seatBid := bidResp.SeatBid[0]
   231  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid))
   232  	bidResponse.Currency = bidResp.Cur
   233  	for i := 0; i < len(seatBid.Bid); i++ {
   234  		bid := seatBid.Bid[i]
   235  		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   236  			Bid:     &bid,
   237  			BidType: getMediaTypeForImpID(bid.ImpID, internalRequest.Imp),
   238  		})
   239  	}
   240  	return bidResponse, nil
   241  }
   242  
   243  // getMediaTypeForImp figures out which media type this bid is for
   244  func getMediaTypeForImpID(impID string, imps []openrtb2.Imp) openrtb_ext.BidType {
   245  	for _, imp := range imps {
   246  		if imp.ID == impID && imp.Banner != nil {
   247  			return openrtb_ext.BidTypeBanner
   248  		}
   249  	}
   250  	return openrtb_ext.BidTypeVideo
   251  }
   252  
   253  func newBadInputError(message string) error {
   254  	return &errortypes.BadInput{
   255  		Message: message,
   256  	}
   257  }
   258  
   259  func newBadServerResponseError(message string) error {
   260  	return &errortypes.BadServerResponse{
   261  		Message: message,
   262  	}
   263  }
   264  
   265  // Builder builds a new instance of the Adkernel adapter for the given bidder with the given config.
   266  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   267  	urlTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint)
   268  	if err != nil {
   269  		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
   270  	}
   271  
   272  	bidder := &adkernelAdapter{
   273  		EndpointTemplate: urlTemplate,
   274  	}
   275  	return bidder, nil
   276  }