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

     1  package grid
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/prebid/openrtb/v20/openrtb2"
    11  	"github.com/prebid/prebid-server/v2/adapters"
    12  	"github.com/prebid/prebid-server/v2/config"
    13  	"github.com/prebid/prebid-server/v2/errortypes"
    14  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    15  	"github.com/prebid/prebid-server/v2/util/maputil"
    16  )
    17  
    18  type GridAdapter struct {
    19  	endpoint string
    20  }
    21  
    22  type GridBid struct {
    23  	*openrtb2.Bid
    24  	AdmNative   json.RawMessage     `json:"adm_native,omitempty"`
    25  	ContentType openrtb_ext.BidType `json:"content_type"`
    26  }
    27  
    28  type GridSeatBid struct {
    29  	*openrtb2.SeatBid
    30  	Bid []GridBid `json:"bid"`
    31  }
    32  
    33  type GridResponse struct {
    34  	*openrtb2.BidResponse
    35  	SeatBid []GridSeatBid `json:"seatbid,omitempty"`
    36  }
    37  
    38  type GridBidExt struct {
    39  	Bidder ExtBidder `json:"bidder"`
    40  }
    41  
    42  type ExtBidder struct {
    43  	Grid ExtBidderGrid `json:"grid"`
    44  }
    45  
    46  type ExtBidderGrid struct {
    47  	DemandSource string `json:"demandSource"`
    48  }
    49  
    50  type ExtImpDataAdServer struct {
    51  	Name   string `json:"name"`
    52  	AdSlot string `json:"adslot"`
    53  }
    54  
    55  type ExtImpData struct {
    56  	PbAdslot string              `json:"pbadslot,omitempty"`
    57  	AdServer *ExtImpDataAdServer `json:"adserver,omitempty"`
    58  }
    59  
    60  type ExtImp struct {
    61  	Prebid  *openrtb_ext.ExtImpPrebid `json:"prebid,omitempty"`
    62  	Bidder  json.RawMessage           `json:"bidder"`
    63  	Data    *ExtImpData               `json:"data,omitempty"`
    64  	Gpid    string                    `json:"gpid,omitempty"`
    65  	Skadn   json.RawMessage           `json:"skadn,omitempty"`
    66  	Context json.RawMessage           `json:"context,omitempty"`
    67  }
    68  
    69  type KeywordSegment struct {
    70  	Name  string `json:"name"`
    71  	Value string `json:"value"`
    72  }
    73  
    74  type KeywordsPublisherItem struct {
    75  	Name     string           `json:"name"`
    76  	Segments []KeywordSegment `json:"segments"`
    77  }
    78  
    79  type KeywordsPublisher map[string][]KeywordsPublisherItem
    80  
    81  type Keywords map[string]KeywordsPublisher
    82  
    83  // buildConsolidatedKeywordsReqExt builds a new request.ext json incorporating request.site.keywords, request.user.keywords,
    84  // and request.imp[0].ext.keywords, and request.ext.keywords. Invalid keywords in request.imp[0].ext.keywords are not incorporated.
    85  // Invalid keywords in request.ext.keywords.site and request.ext.keywords.user are dropped.
    86  func buildConsolidatedKeywordsReqExt(openRTBUser, openRTBSite string, firstImpExt, requestExt json.RawMessage) (json.RawMessage, error) {
    87  	// unmarshal ext to object map
    88  	requestExtMap := parseExtToMap(requestExt)
    89  	firstImpExtMap := parseExtToMap(firstImpExt)
    90  	// extract `keywords` field
    91  	requestExtKeywordsMap := extractKeywordsMap(requestExtMap)
    92  	firstImpExtKeywordsMap := extractBidderKeywordsMap(firstImpExtMap)
    93  	// parse + merge keywords
    94  	keywords := parseKeywordsFromMap(requestExtKeywordsMap)                // request.ext.keywords
    95  	mergeKeywords(keywords, parseKeywordsFromMap(firstImpExtKeywordsMap))  // request.imp[0].ext.bidder.keywords
    96  	mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBUser, "user")) // request.user.keywords
    97  	mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBSite, "site")) // request.site.keywords
    98  
    99  	// overlay site + user keywords
   100  	if site, exists := keywords["site"]; exists && len(site) > 0 {
   101  		requestExtKeywordsMap["site"] = site
   102  	} else {
   103  		delete(requestExtKeywordsMap, "site")
   104  	}
   105  	if user, exists := keywords["user"]; exists && len(user) > 0 {
   106  		requestExtKeywordsMap["user"] = user
   107  	} else {
   108  		delete(requestExtKeywordsMap, "user")
   109  	}
   110  	// reconcile keywords with request.ext
   111  	if len(requestExtKeywordsMap) > 0 {
   112  		requestExtMap["keywords"] = requestExtKeywordsMap
   113  	} else {
   114  		delete(requestExtMap, "keywords")
   115  	}
   116  	// marshal final result
   117  	if len(requestExtMap) > 0 {
   118  		return json.Marshal(requestExtMap)
   119  	}
   120  	return nil, nil
   121  }
   122  func parseExtToMap(ext json.RawMessage) map[string]interface{} {
   123  	var root map[string]interface{}
   124  	if err := json.Unmarshal(ext, &root); err != nil {
   125  		return make(map[string]interface{})
   126  	}
   127  	return root
   128  }
   129  func extractKeywordsMap(ext map[string]interface{}) map[string]interface{} {
   130  	if keywords, exists := maputil.ReadEmbeddedMap(ext, "keywords"); exists {
   131  		return keywords
   132  	}
   133  	return make(map[string]interface{})
   134  }
   135  func extractBidderKeywordsMap(ext map[string]interface{}) map[string]interface{} {
   136  	if bidder, exists := maputil.ReadEmbeddedMap(ext, "bidder"); exists {
   137  		return extractKeywordsMap(bidder)
   138  	}
   139  	return make(map[string]interface{})
   140  }
   141  func parseKeywordsFromMap(extKeywords map[string]interface{}) Keywords {
   142  	keywords := make(Keywords)
   143  	for k, v := range extKeywords {
   144  		// keywords may only be provided in the site and user sections
   145  		if k != "site" && k != "user" {
   146  			continue
   147  		}
   148  		// the site or user sections must be an object
   149  		if section, ok := v.(map[string]interface{}); ok {
   150  			keywords[k] = parseKeywordsFromSection(section)
   151  		}
   152  	}
   153  	return keywords
   154  }
   155  func parseKeywordsFromSection(section map[string]interface{}) KeywordsPublisher {
   156  	keywordsPublishers := make(KeywordsPublisher)
   157  	for publisherKey, publisherValue := range section {
   158  		// publisher value must be a slice
   159  		publisherValueSlice, ok := publisherValue.([]interface{})
   160  		if !ok {
   161  			continue
   162  		}
   163  		for _, publisherValueItem := range publisherValueSlice {
   164  			// item must be an object
   165  			publisherItem, ok := publisherValueItem.(map[string]interface{})
   166  			if !ok {
   167  				continue
   168  			}
   169  			// publisher item must have a name
   170  			publisherName, ok := maputil.ReadEmbeddedString(publisherItem, "name")
   171  			if !ok {
   172  				continue
   173  			}
   174  			var segments []KeywordSegment
   175  			// extract valid segments
   176  			if segmentsSlice, exists := maputil.ReadEmbeddedSlice(publisherItem, "segments"); exists {
   177  				for _, segment := range segmentsSlice {
   178  					if segmentMap, ok := segment.(map[string]interface{}); ok {
   179  						name, hasName := maputil.ReadEmbeddedString(segmentMap, "name")
   180  						value, hasValue := maputil.ReadEmbeddedString(segmentMap, "value")
   181  						if hasName && hasValue {
   182  							segments = append(segments, KeywordSegment{Name: name, Value: value})
   183  						}
   184  					}
   185  				}
   186  			}
   187  			// ensure consistent ordering for publisher item map
   188  			publisherItemKeys := make([]string, 0, len(publisherItem))
   189  			for v := range publisherItem {
   190  				publisherItemKeys = append(publisherItemKeys, v)
   191  			}
   192  			sort.Strings(publisherItemKeys)
   193  			// compose compatible alternate segment format
   194  			for _, potentialSegmentName := range publisherItemKeys {
   195  				potentialSegmentValues := publisherItem[potentialSegmentName]
   196  				// values must be an array
   197  				if valuesSlice, ok := potentialSegmentValues.([]interface{}); ok {
   198  					for _, value := range valuesSlice {
   199  						if valueAsString, ok := value.(string); ok {
   200  							segments = append(segments, KeywordSegment{Name: potentialSegmentName, Value: valueAsString})
   201  						}
   202  					}
   203  				}
   204  			}
   205  			if len(segments) > 0 {
   206  				keywordsPublishers[publisherKey] = append(keywordsPublishers[publisherKey], KeywordsPublisherItem{Name: publisherName, Segments: segments})
   207  			}
   208  		}
   209  	}
   210  	return keywordsPublishers
   211  }
   212  func parseKeywordsFromOpenRTB(keywords, section string) Keywords {
   213  	keywordsSplit := strings.Split(keywords, ",")
   214  	segments := make([]KeywordSegment, 0, len(keywordsSplit))
   215  	for _, v := range keywordsSplit {
   216  		if v != "" {
   217  			segments = append(segments, KeywordSegment{Name: "keywords", Value: v})
   218  		}
   219  	}
   220  	if len(segments) > 0 {
   221  		return map[string]KeywordsPublisher{section: map[string][]KeywordsPublisherItem{"ortb2": {{Name: "keywords", Segments: segments}}}}
   222  	}
   223  	return make(Keywords)
   224  }
   225  func mergeKeywords(a, b Keywords) {
   226  	for key, values := range b {
   227  		if _, sectionExists := a[key]; !sectionExists {
   228  			a[key] = KeywordsPublisher{}
   229  		}
   230  		for publisherKey, publisherValues := range values {
   231  			a[key][publisherKey] = append(publisherValues, a[key][publisherKey]...)
   232  		}
   233  	}
   234  }
   235  
   236  func setImpExtKeywords(request *openrtb2.BidRequest) error {
   237  	userKeywords := ""
   238  	if request.User != nil {
   239  		userKeywords = request.User.Keywords
   240  	}
   241  	siteKeywords := ""
   242  	if request.Site != nil {
   243  		siteKeywords = request.Site.Keywords
   244  	}
   245  	var err error
   246  	request.Ext, err = buildConsolidatedKeywordsReqExt(userKeywords, siteKeywords, request.Imp[0].Ext, request.Ext)
   247  	return err
   248  }
   249  
   250  func processImp(imp *openrtb2.Imp) error {
   251  	// get the grid extension
   252  	var ext adapters.ExtImpBidder
   253  	var gridExt openrtb_ext.ExtImpGrid
   254  	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
   255  		return err
   256  	}
   257  	if err := json.Unmarshal(ext.Bidder, &gridExt); err != nil {
   258  		return err
   259  	}
   260  
   261  	if gridExt.Uid == 0 {
   262  		err := &errortypes.BadInput{
   263  			Message: "uid is empty",
   264  		}
   265  		return err
   266  	}
   267  	// no error
   268  	return nil
   269  }
   270  
   271  func setImpExtData(imp openrtb2.Imp) openrtb2.Imp {
   272  	var ext ExtImp
   273  	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
   274  		return imp
   275  	}
   276  	if ext.Data != nil && ext.Data.AdServer != nil && ext.Data.AdServer.AdSlot != "" {
   277  		ext.Gpid = ext.Data.AdServer.AdSlot
   278  		extJSON, err := json.Marshal(ext)
   279  		if err == nil {
   280  			imp.Ext = extJSON
   281  		}
   282  	}
   283  	return imp
   284  }
   285  
   286  func fixNative(req json.RawMessage) (json.RawMessage, error) {
   287  	var gridReq map[string]interface{}
   288  	var parsedRequest map[string]interface{}
   289  
   290  	if err := json.Unmarshal(req, &gridReq); err != nil {
   291  		return req, nil
   292  	}
   293  	if imps, exists := maputil.ReadEmbeddedSlice(gridReq, "imp"); exists {
   294  		for _, imp := range imps {
   295  			if gridImp, ok := imp.(map[string]interface{}); ok {
   296  				native, hasNative := maputil.ReadEmbeddedMap(gridImp, "native")
   297  				if hasNative {
   298  					request, hasRequest := maputil.ReadEmbeddedString(native, "request")
   299  					if hasRequest {
   300  						delete(native, "request")
   301  						if err := json.Unmarshal([]byte(request), &parsedRequest); err == nil {
   302  							native["request_native"] = parsedRequest
   303  						} else {
   304  							native["request_native"] = request
   305  						}
   306  					}
   307  				}
   308  			}
   309  		}
   310  	}
   311  
   312  	return json.Marshal(gridReq)
   313  }
   314  
   315  // MakeRequests makes the HTTP requests which should be made to fetch bids.
   316  func (a *GridAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
   317  	var errors = make([]error, 0)
   318  
   319  	// this will contain all the valid impressions
   320  	var validImps []openrtb2.Imp
   321  	// pre-process the imps
   322  	for _, imp := range request.Imp {
   323  		if err := processImp(&imp); err == nil {
   324  			validImps = append(validImps, setImpExtData(imp))
   325  		} else {
   326  			errors = append(errors, err)
   327  		}
   328  	}
   329  	if len(validImps) == 0 {
   330  		err := &errortypes.BadInput{
   331  			Message: "No valid impressions for grid",
   332  		}
   333  		errors = append(errors, err)
   334  		return nil, errors
   335  	}
   336  
   337  	if err := setImpExtKeywords(request); err != nil {
   338  		errors = append(errors, err)
   339  		return nil, errors
   340  	}
   341  
   342  	request.Imp = validImps
   343  
   344  	reqJSON, err := json.Marshal(request)
   345  
   346  	if err != nil {
   347  		errors = append(errors, err)
   348  		return nil, errors
   349  	}
   350  
   351  	fixedReqJSON, err := fixNative(reqJSON)
   352  
   353  	if err != nil {
   354  		errors = append(errors, err)
   355  		return nil, errors
   356  	}
   357  
   358  	headers := http.Header{}
   359  	headers.Add("Content-Type", "application/json;charset=utf-8")
   360  
   361  	return []*adapters.RequestData{{
   362  		Method:  "POST",
   363  		Uri:     a.endpoint,
   364  		Body:    fixedReqJSON,
   365  		Headers: headers,
   366  		ImpIDs:  openrtb_ext.GetImpIDs(request.Imp),
   367  	}}, errors
   368  }
   369  
   370  // MakeBids unpacks the server's response into Bids.
   371  func (a *GridAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   372  	if response.StatusCode == http.StatusNoContent {
   373  		return nil, nil
   374  	}
   375  
   376  	if response.StatusCode == http.StatusBadRequest {
   377  		return nil, []error{&errortypes.BadInput{
   378  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   379  		}}
   380  	}
   381  
   382  	if response.StatusCode != http.StatusOK {
   383  		return nil, []error{&errortypes.BadServerResponse{
   384  			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
   385  		}}
   386  	}
   387  
   388  	var bidResp GridResponse
   389  	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
   390  		return nil, []error{err}
   391  	}
   392  
   393  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(1)
   394  
   395  	for _, sb := range bidResp.SeatBid {
   396  		for i := range sb.Bid {
   397  			bidMeta, err := getBidMeta(sb.Bid[i].Ext)
   398  			bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp, sb.Bid[i])
   399  			if sb.Bid[i].AdmNative != nil && sb.Bid[i].AdM == "" {
   400  				if bytes, err := json.Marshal(sb.Bid[i].AdmNative); err == nil {
   401  					sb.Bid[i].AdM = string(bytes)
   402  				}
   403  			}
   404  			if err != nil {
   405  				return nil, []error{err}
   406  			}
   407  
   408  			openrtb2Bid := sb.Bid[i].Bid
   409  
   410  			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   411  				Bid:     openrtb2Bid,
   412  				BidType: bidType,
   413  				BidMeta: bidMeta,
   414  			})
   415  		}
   416  	}
   417  	return bidResponse, nil
   418  
   419  }
   420  
   421  // Builder builds a new instance of the Grid adapter for the given bidder with the given config.
   422  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
   423  	bidder := &GridAdapter{
   424  		endpoint: config.Endpoint,
   425  	}
   426  	return bidder, nil
   427  }
   428  
   429  func getBidMeta(ext json.RawMessage) (*openrtb_ext.ExtBidPrebidMeta, error) {
   430  	var bidExt GridBidExt
   431  
   432  	if err := json.Unmarshal(ext, &bidExt); err != nil {
   433  		return nil, err
   434  	}
   435  	var bidMeta *openrtb_ext.ExtBidPrebidMeta
   436  	if bidExt.Bidder.Grid.DemandSource != "" {
   437  		bidMeta = &openrtb_ext.ExtBidPrebidMeta{
   438  			NetworkName: bidExt.Bidder.Grid.DemandSource,
   439  		}
   440  	}
   441  	return bidMeta, nil
   442  }
   443  
   444  func getMediaTypeForImp(impID string, imps []openrtb2.Imp, bidWithType GridBid) (openrtb_ext.BidType, error) {
   445  	if bidWithType.ContentType != "" {
   446  		return bidWithType.ContentType, nil
   447  	} else {
   448  		for _, imp := range imps {
   449  			if imp.ID == impID {
   450  				if imp.Banner != nil {
   451  					return openrtb_ext.BidTypeBanner, nil
   452  				}
   453  
   454  				if imp.Video != nil {
   455  					return openrtb_ext.BidTypeVideo, nil
   456  				}
   457  
   458  				if imp.Native != nil {
   459  					return openrtb_ext.BidTypeNative, nil
   460  				}
   461  
   462  				return "", &errortypes.BadServerResponse{
   463  					Message: fmt.Sprintf("Unknown impression type for ID: \"%s\"", impID),
   464  				}
   465  			}
   466  		}
   467  	}
   468  
   469  	// This shouldnt happen. Lets handle it just incase by returning an error.
   470  	return "", &errortypes.BadServerResponse{
   471  		Message: fmt.Sprintf("Failed to find impression for ID: \"%s\"", impID),
   472  	}
   473  }