github.com/prebid/prebid-server/v2@v2.18.0/adapters/improvedigital/improvedigital.go (about)

     1  package improvedigital
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/prebid/openrtb/v20/openrtb2"
    12  	"github.com/prebid/prebid-server/v2/adapters"
    13  	"github.com/prebid/prebid-server/v2/config"
    14  	"github.com/prebid/prebid-server/v2/errortypes"
    15  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    16  )
    17  
    18  const (
    19  	isRewardedInventory              = "is_rewarded_inventory"
    20  	stateRewardedInventoryEnable     = "1"
    21  	consentProvidersSettingsInputKey = "ConsentedProvidersSettings"
    22  	consentProvidersSettingsOutKey   = "consented_providers_settings"
    23  	consentedProvidersKey            = "consented_providers"
    24  	publisherEndpointParam           = "{PublisherId}"
    25  )
    26  
    27  type ImprovedigitalAdapter struct {
    28  	endpoint string
    29  }
    30  
    31  // BidExt represents Improved Digital bid extension with line item ID and buying type values
    32  type BidExt struct {
    33  	Improvedigital struct {
    34  		LineItemID int    `json:"line_item_id"`
    35  		BuyingType string `json:"buying_type"`
    36  	}
    37  }
    38  
    39  // ImpExtBidder represents Improved Digital bid extension with Publisher ID
    40  type ImpExtBidder struct {
    41  	Bidder struct {
    42  		PublisherID int `json:"publisherId"`
    43  	}
    44  }
    45  
    46  var dealDetectionRegEx = regexp.MustCompile("(classic|deal)")
    47  
    48  // MakeRequests makes the HTTP requests which should be made to fetch bids.
    49  func (a *ImprovedigitalAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    50  	numRequests := len(request.Imp)
    51  	errors := make([]error, 0)
    52  	adapterRequests := make([]*adapters.RequestData, 0, numRequests)
    53  
    54  	// Split multi-imp request into multiple ad server requests. SRA is currently not recommended.
    55  	for i := 0; i < numRequests; i++ {
    56  		if adapterReq, err := a.makeRequest(*request, request.Imp[i]); err == nil {
    57  			adapterRequests = append(adapterRequests, adapterReq)
    58  		} else {
    59  			errors = append(errors, err)
    60  		}
    61  	}
    62  
    63  	return adapterRequests, errors
    64  }
    65  
    66  func (a *ImprovedigitalAdapter) makeRequest(request openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
    67  	// Handle Rewarded Inventory
    68  	impExt, err := getImpExtWithRewardedInventory(imp)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if impExt != nil {
    73  		imp.Ext = impExt
    74  	}
    75  
    76  	request.Imp = []openrtb2.Imp{imp}
    77  
    78  	userExtAddtlConsent, err := a.getAdditionalConsentProvidersUserExt(request)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	if len(userExtAddtlConsent) > 0 {
    84  		userCopy := *request.User
    85  		userCopy.Ext = userExtAddtlConsent
    86  		request.User = &userCopy
    87  	}
    88  
    89  	reqJSON, err := json.Marshal(request)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	headers := http.Header{}
    95  	headers.Add("Content-Type", "application/json;charset=utf-8")
    96  
    97  	return &adapters.RequestData{
    98  		Method:  "POST",
    99  		Uri:     a.buildEndpointURL(imp),
   100  		Body:    reqJSON,
   101  		Headers: headers,
   102  		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   103  	}, nil
   104  }
   105  
   106  // MakeBids unpacks the server's response into Bids.
   107  func (a *ImprovedigitalAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   108  	if response.StatusCode == http.StatusNoContent {
   109  		return nil, nil
   110  	}
   111  
   112  	if response.StatusCode == http.StatusBadRequest {
   113  		return nil, []error{&errortypes.BadInput{
   114  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   115  		}}
   116  	}
   117  
   118  	if response.StatusCode != http.StatusOK {
   119  		return nil, []error{&errortypes.BadServerResponse{
   120  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   121  		}}
   122  	}
   123  
   124  	var bidResp openrtb2.BidResponse
   125  	var impMap = make(map[string]openrtb2.Imp)
   126  	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
   127  		return nil, []error{err}
   128  	}
   129  
   130  	if len(bidResp.SeatBid) == 0 {
   131  		return nil, nil
   132  	}
   133  
   134  	if len(bidResp.SeatBid) > 1 {
   135  		return nil, []error{&errortypes.BadServerResponse{
   136  			Message: fmt.Sprintf("Unexpected SeatBid! Must be only one but have: %d", len(bidResp.SeatBid)),
   137  		}}
   138  	}
   139  
   140  	seatBid := bidResp.SeatBid[0]
   141  	if len(seatBid.Bid) == 0 {
   142  		return nil, nil
   143  	}
   144  
   145  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seatBid.Bid))
   146  	bidResponse.Currency = bidResp.Cur
   147  
   148  	for i := range internalRequest.Imp {
   149  		impMap[internalRequest.Imp[i].ID] = internalRequest.Imp[i]
   150  	}
   151  
   152  	for i := range seatBid.Bid {
   153  		bid := seatBid.Bid[i]
   154  
   155  		bidType, err := getBidType(bid, impMap)
   156  		if err != nil {
   157  			return nil, []error{err}
   158  		}
   159  
   160  		if bid.Ext != nil {
   161  			var bidExt BidExt
   162  			err = json.Unmarshal(bid.Ext, &bidExt)
   163  			if err != nil {
   164  				return nil, []error{err}
   165  			}
   166  
   167  			bidExtImprovedigital := bidExt.Improvedigital
   168  			if bidExtImprovedigital.LineItemID != 0 && dealDetectionRegEx.MatchString(bidExtImprovedigital.BuyingType) {
   169  				bid.DealID = strconv.Itoa(bidExtImprovedigital.LineItemID)
   170  			}
   171  		}
   172  
   173  		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   174  			Bid:     &bid,
   175  			BidType: bidType,
   176  		})
   177  	}
   178  	return bidResponse, nil
   179  }
   180  
   181  // Builder builds a new instance of the Improvedigital adapter for the given bidder with the given config.
   182  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   183  	bidder := &ImprovedigitalAdapter{
   184  		endpoint: config.Endpoint,
   185  	}
   186  	return bidder, nil
   187  }
   188  
   189  func getBidType(bid openrtb2.Bid, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) {
   190  	// there must be a matching imp against bid.ImpID
   191  	imp, found := impMap[bid.ImpID]
   192  	if !found {
   193  		return "", &errortypes.BadServerResponse{
   194  			Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", bid.ImpID),
   195  		}
   196  	}
   197  
   198  	// if MType is not set in server response, try to determine it
   199  	if bid.MType == 0 {
   200  		if !isMultiFormatImp(imp) {
   201  			// Not a bid for multi format impression. So, determine MType from impression
   202  			if imp.Banner != nil {
   203  				bid.MType = openrtb2.MarkupBanner
   204  			} else if imp.Video != nil {
   205  				bid.MType = openrtb2.MarkupVideo
   206  			} else if imp.Audio != nil {
   207  				bid.MType = openrtb2.MarkupAudio
   208  			} else if imp.Native != nil {
   209  				bid.MType = openrtb2.MarkupNative
   210  			} else { // This should not happen.
   211  				// Let's handle it just in case by returning an error.
   212  				return "", &errortypes.BadServerResponse{
   213  					Message: fmt.Sprintf("Could not determine MType from impression with ID: \"%s\"", bid.ImpID),
   214  				}
   215  			}
   216  		} else {
   217  			return "", &errortypes.BadServerResponse{
   218  				Message: fmt.Sprintf("Bid must have non-zero MType for multi format impression with ID: \"%s\"", bid.ImpID),
   219  			}
   220  		}
   221  	}
   222  
   223  	// map MType to BidType
   224  	switch bid.MType {
   225  	case openrtb2.MarkupBanner:
   226  		return openrtb_ext.BidTypeBanner, nil
   227  	case openrtb2.MarkupVideo:
   228  		return openrtb_ext.BidTypeVideo, nil
   229  	case openrtb2.MarkupAudio:
   230  		return openrtb_ext.BidTypeAudio, nil
   231  	case openrtb2.MarkupNative:
   232  		return openrtb_ext.BidTypeNative, nil
   233  	default:
   234  		// This shouldn't happen. Let's handle it just in case by returning an error.
   235  		return "", &errortypes.BadServerResponse{
   236  			Message: fmt.Sprintf("Unsupported MType %d for impression with ID: \"%s\"", bid.MType, bid.ImpID),
   237  		}
   238  	}
   239  }
   240  
   241  func isMultiFormatImp(imp openrtb2.Imp) bool {
   242  	formatCount := 0
   243  	if imp.Banner != nil {
   244  		formatCount++
   245  	}
   246  	if imp.Video != nil {
   247  		formatCount++
   248  	}
   249  	if imp.Audio != nil {
   250  		formatCount++
   251  	}
   252  	if imp.Native != nil {
   253  		formatCount++
   254  	}
   255  	return formatCount > 1
   256  }
   257  
   258  // This method responsible to clone request and convert additional consent providers string to array when additional consent provider found
   259  func (a *ImprovedigitalAdapter) getAdditionalConsentProvidersUserExt(request openrtb2.BidRequest) ([]byte, error) {
   260  	var cpStr string
   261  
   262  	// If user/user.ext not defined, no need to parse additional consent
   263  	if request.User == nil || request.User.Ext == nil {
   264  		return nil, nil
   265  	}
   266  
   267  	// Start validating additional consent
   268  	// Check key exist user.ext.ConsentedProvidersSettings
   269  	var userExtMap = make(map[string]json.RawMessage)
   270  	if err := json.Unmarshal(request.User.Ext, &userExtMap); err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	cpsMapValue, cpsJSONFound := userExtMap[consentProvidersSettingsInputKey]
   275  	if !cpsJSONFound {
   276  		return nil, nil
   277  	}
   278  
   279  	// Check key exist user.ext.ConsentedProvidersSettings.consented_providers
   280  	var cpMap = make(map[string]json.RawMessage)
   281  	if err := json.Unmarshal(cpsMapValue, &cpMap); err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	cpMapValue, cpJSONFound := cpMap[consentedProvidersKey]
   286  	if !cpJSONFound {
   287  		return nil, nil
   288  	}
   289  	// End validating additional consent
   290  
   291  	// Trim enclosing quotes after casting json.RawMessage to string
   292  	consentStr := strings.Trim((string)(cpMapValue), "\"")
   293  	// Split by ~ and take only the second string (if exists) as the consented providers spec
   294  	var consentStrParts = strings.Split(consentStr, "~")
   295  	if len(consentStrParts) < 2 {
   296  		return nil, nil
   297  	}
   298  	cpStr = strings.TrimSpace(consentStrParts[1])
   299  	if len(cpStr) == 0 {
   300  		return nil, nil
   301  	}
   302  
   303  	// Prepare consent providers string
   304  	cpStr = fmt.Sprintf("[%s]", strings.Replace(cpStr, ".", ",", -1))
   305  	cpMap[consentedProvidersKey] = json.RawMessage(cpStr)
   306  
   307  	cpJSON, err := json.Marshal(cpMap)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	userExtMap[consentProvidersSettingsOutKey] = cpJSON
   312  
   313  	extJson, err := json.Marshal(userExtMap)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	return extJson, nil
   319  }
   320  
   321  func getImpExtWithRewardedInventory(imp openrtb2.Imp) ([]byte, error) {
   322  	var ext = make(map[string]json.RawMessage)
   323  	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
   324  		return nil, err
   325  	}
   326  
   327  	prebidJSONValue, prebidJSONFound := ext["prebid"]
   328  	if !prebidJSONFound {
   329  		return nil, nil
   330  	}
   331  
   332  	var prebidMap = make(map[string]json.RawMessage)
   333  	if err := json.Unmarshal(prebidJSONValue, &prebidMap); err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	if rewardedInventory, foundRewardedInventory := prebidMap[isRewardedInventory]; foundRewardedInventory && string(rewardedInventory) == stateRewardedInventoryEnable {
   338  		ext[isRewardedInventory] = json.RawMessage(`true`)
   339  		impExt, err := json.Marshal(ext)
   340  		if err != nil {
   341  			return nil, err
   342  		}
   343  
   344  		return impExt, nil
   345  	}
   346  
   347  	return nil, nil
   348  }
   349  
   350  func (a *ImprovedigitalAdapter) buildEndpointURL(imp openrtb2.Imp) string {
   351  	publisherEndpoint := ""
   352  	var impBidder ImpExtBidder
   353  
   354  	err := json.Unmarshal(imp.Ext, &impBidder)
   355  	if err == nil && impBidder.Bidder.PublisherID != 0 {
   356  		publisherEndpoint = strconv.Itoa(impBidder.Bidder.PublisherID) + "/"
   357  	}
   358  
   359  	return strings.Replace(a.endpoint, publisherEndpointParam, publisherEndpoint, -1)
   360  }