github.com/prebid/prebid-server/v2@v2.18.0/adapters/huaweiads/huaweiads.go (about) 1 package huaweiads 2 3 import ( 4 "bytes" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "math" 12 "net/http" 13 "net/url" 14 "regexp" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/prebid/openrtb/v20/native1" 20 nativeRequests "github.com/prebid/openrtb/v20/native1/request" 21 nativeResponse "github.com/prebid/openrtb/v20/native1/response" 22 "github.com/prebid/openrtb/v20/openrtb2" 23 "github.com/prebid/prebid-server/v2/adapters" 24 "github.com/prebid/prebid-server/v2/config" 25 "github.com/prebid/prebid-server/v2/errortypes" 26 "github.com/prebid/prebid-server/v2/openrtb_ext" 27 "github.com/prebid/prebid-server/v2/util/ptrutil" 28 ) 29 30 const huaweiAdxApiVersion = "3.4" 31 const defaultCountryName = "ZA" 32 const defaultUnknownNetworkType = 0 33 const timeFormat = "2006-01-02 15:04:05.000" 34 const defaultTimeZone = "+0200" 35 const defaultModelName = "HUAWEI" 36 const chineseSiteEndPoint = "https://acd.op.hicloud.com/ppsadx/getResult" 37 const europeanSiteEndPoint = "https://adx-dre.op.hicloud.com/ppsadx/getResult" 38 const asianSiteEndPoint = "https://adx-dra.op.hicloud.com/ppsadx/getResult" 39 const russianSiteEndPoint = "https://adx-drru.op.hicloud.com/ppsadx/getResult" 40 41 // creative type 42 const ( 43 text int32 = 1 44 bigPicture int32 = 2 45 bigPicture2 int32 = 3 46 gif int32 = 4 47 videoText int32 = 6 48 smallPicture int32 = 7 49 threeSmallPicturesText int32 = 8 50 video int32 = 9 51 iconText int32 = 10 52 videoWithPicturesText int32 = 11 53 ) 54 55 // interaction type 56 const ( 57 appPromotion int32 = 3 58 ) 59 60 // ads type 61 const ( 62 banner int32 = 8 63 native int32 = 3 64 roll int32 = 60 65 interstitial int32 = 12 66 rewarded int32 = 7 67 splash int32 = 1 68 magazinelock int32 = 2 69 audio int32 = 17 70 ) 71 72 type huaweiAdsRequest struct { 73 Version string `json:"version"` 74 Multislot []adslot30 `json:"multislot"` 75 App app `json:"app"` 76 Device device `json:"device"` 77 Network network `json:"network,omitempty"` 78 Regs regs `json:"regs,omitempty"` 79 Geo geo `json:"geo,omitempty"` 80 Consent string `json:"consent,omitempty"` 81 ClientAdRequestId string `json:"clientAdRequestId,omitempty"` 82 } 83 84 type adslot30 struct { 85 Slotid string `json:"slotid"` 86 Adtype int32 `json:"adtype"` 87 Test int32 `json:"test"` 88 TotalDuration int32 `json:"totalDuration,omitempty"` 89 Orientation int32 `json:"orientation,omitempty"` 90 W int64 `json:"w,omitempty"` 91 H int64 `json:"h,omitempty"` 92 Format []format `json:"format,omitempty"` 93 DetailedCreativeTypeList []string `json:"detailedCreativeTypeList,omitempty"` 94 } 95 96 type format struct { 97 W int64 `json:"w,omitempty"` 98 H int64 `json:"h,omitempty"` 99 } 100 101 type app struct { 102 Version string `json:"version,omitempty"` 103 Name string `json:"name,omitempty"` 104 Pkgname string `json:"pkgname"` 105 Lang string `json:"lang,omitempty"` 106 Country string `json:"country,omitempty"` 107 } 108 109 type device struct { 110 Type int32 `json:"type,omitempty"` 111 Useragent string `json:"useragent,omitempty"` 112 Os string `json:"os,omitempty"` 113 Version string `json:"version,omitempty"` 114 Maker string `json:"maker,omitempty"` 115 Model string `json:"model,omitempty"` 116 Width int32 `json:"width,omitempty"` 117 Height int32 `json:"height,omitempty"` 118 Language string `json:"language,omitempty"` 119 BuildVersion string `json:"buildVersion,omitempty"` 120 Dpi int32 `json:"dpi,omitempty"` 121 Pxratio float32 `json:"pxratio,omitempty"` 122 Imei string `json:"imei,omitempty"` 123 Oaid string `json:"oaid,omitempty"` 124 IsTrackingEnabled string `json:"isTrackingEnabled,omitempty"` 125 EmuiVer string `json:"emuiVer,omitempty"` 126 LocaleCountry string `json:"localeCountry"` 127 BelongCountry string `json:"belongCountry"` 128 GaidTrackingEnabled string `json:"gaidTrackingEnabled,omitempty"` 129 Gaid string `json:"gaid,omitempty"` 130 ClientTime string `json:"clientTime"` 131 Ip string `json:"ip,omitempty"` 132 } 133 134 type network struct { 135 Type int32 `json:"type"` 136 Carrier int32 `json:"carrier,omitempty"` 137 CellInfo []cellInfo `json:"cellInfo,omitempty"` 138 } 139 140 type regs struct { 141 Coppa int32 `json:"coppa,omitempty"` 142 } 143 144 type geo struct { 145 Lon float32 `json:"lon,omitempty"` 146 Lat float32 `json:"lat,omitempty"` 147 Accuracy int32 `json:"accuracy,omitempty"` 148 Lastfix int32 `json:"lastfix,omitempty"` 149 } 150 151 type cellInfo struct { 152 Mcc string `json:"mcc,omitempty"` 153 Mnc string `json:"mnc,omitempty"` 154 } 155 156 type huaweiAdsResponse struct { 157 Retcode int32 `json:"retcode"` 158 Reason string `json:"reason"` 159 Multiad []ad30 `json:"multiad"` 160 } 161 162 type ad30 struct { 163 AdType int32 `json:"adtype"` 164 Slotid string `json:"slotid"` 165 Retcode30 int32 `json:"retcode30"` 166 Content []content `json:"content"` 167 } 168 169 type content struct { 170 Contentid string `json:"contentid"` 171 Interactiontype int32 `json:"interactiontype"` 172 Creativetype int32 `json:"creativetype"` 173 MetaData metaData `json:"metaData"` 174 Monitor []monitor `json:"monitor"` 175 Cur string `json:"cur"` 176 Price float64 `json:"price"` 177 } 178 179 type metaData struct { 180 Title string `json:"title"` 181 Description string `json:"description"` 182 ImageInfo []imageInfo `json:"imageInfo"` 183 Icon []icon `json:"icon"` 184 ClickUrl string `json:"clickUrl"` 185 Intent string `json:"intent"` 186 VideoInfo videoInfo `json:"videoInfo"` 187 ApkInfo apkInfo `json:"apkInfo"` 188 Duration int64 `json:"duration"` 189 MediaFile mediaFile `json:"mediaFile"` 190 Cta string `json:"cta"` 191 } 192 193 type imageInfo struct { 194 Url string `json:"url"` 195 Height int64 `json:"height"` 196 FileSize int64 `json:"fileSize"` 197 Sha256 string `json:"sha256"` 198 ImageType string `json:"imageType"` 199 Width int64 `json:"width"` 200 } 201 202 type icon struct { 203 Url string `json:"url"` 204 Height int64 `json:"height"` 205 FileSize int64 `json:"fileSize"` 206 Sha256 string `json:"sha256"` 207 ImageType string `json:"imageType"` 208 Width int64 `json:"width"` 209 } 210 211 type videoInfo struct { 212 VideoDownloadUrl string `json:"videoDownloadUrl"` 213 VideoDuration int32 `json:"videoDuration"` 214 VideoFileSize int32 `json:"videoFileSize"` 215 Sha256 string `json:"sha256"` 216 VideoRatio float32 `json:"videoRatio"` 217 Width int32 `json:"width"` 218 Height int32 `json:"height"` 219 } 220 221 type apkInfo struct { 222 Url string `json:"url"` 223 FileSize int64 `json:"fileSize"` 224 Sha256 string `json:"sha256"` 225 PackageName string `json:"packageName"` 226 SecondUrl string `json:"secondUrl"` 227 AppName string `json:"appName"` 228 VersionName string `json:"versionName"` 229 AppDesc string `json:"appDesc"` 230 AppIcon string `json:"appIcon"` 231 } 232 233 type mediaFile struct { 234 Mime string `json:"mime"` 235 Width int64 `json:"width"` 236 Height int64 `json:"height"` 237 FileSize int64 `json:"fileSize"` 238 Url string `json:"url"` 239 Sha256 string `json:"sha256"` 240 } 241 242 type monitor struct { 243 EventType string `json:"eventType"` 244 Url []string `json:"url"` 245 } 246 247 type adapter struct { 248 endpoint string 249 extraInfo ExtraInfo 250 } 251 252 type ExtraInfo struct { 253 PkgNameConvert []pkgNameConvert `json:"pkgNameConvert,omitempty"` 254 CloseSiteSelectionByCountry string `json:"closeSiteSelectionByCountry,omitempty"` 255 } 256 257 type pkgNameConvert struct { 258 ConvertedPkgName string `json:"convertedPkgName,omitempty"` 259 UnconvertedPkgNames []string `json:"unconvertedPkgNames,omitempty"` 260 UnconvertedPkgNameKeyWords []string `json:"unconvertedPkgNameKeyWords,omitempty"` 261 UnconvertedPkgNamePrefixs []string `json:"unconvertedPkgNamePrefixs,omitempty"` 262 ExceptionPkgNames []string `json:"exceptionPkgNames,omitempty"` 263 } 264 265 type empty struct { 266 } 267 268 func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, 269 reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) { 270 // the upstream code already confirms that there is a non-zero number of impressions 271 numRequests := len(openRTBRequest.Imp) 272 var request huaweiAdsRequest 273 var header http.Header 274 var multislot = make([]adslot30, 0, numRequests) 275 276 var huaweiAdsImpExt *openrtb_ext.ExtImpHuaweiAds 277 for index := 0; index < numRequests; index++ { 278 var err1 error 279 huaweiAdsImpExt, err1 = unmarshalExtImpHuaweiAds(&openRTBRequest.Imp[index]) 280 if err1 != nil { 281 return nil, []error{err1} 282 } 283 284 if huaweiAdsImpExt == nil { 285 return nil, []error{errors.New("Unmarshal ExtImpHuaweiAds failed: huaweiAdsImpExt is nil.")} 286 } 287 288 adslot30, err := getReqAdslot30(huaweiAdsImpExt, &openRTBRequest.Imp[index]) 289 if err != nil { 290 return nil, []error{err} 291 } 292 293 multislot = append(multislot, adslot30) 294 } 295 request.Multislot = multislot 296 request.ClientAdRequestId = openRTBRequest.ID 297 298 countryCode, err := getReqJson(&request, openRTBRequest, a.extraInfo) 299 if err != nil { 300 return nil, []error{err} 301 } 302 303 reqJSON, err := json.Marshal(request) 304 if err != nil { 305 return nil, []error{err} 306 } 307 308 // our request header's Authorization is changing by time, cannot verify by a certain string, 309 // use isTestAuthorization = true only when run testcase 310 var isTestAuthorization = false 311 if huaweiAdsImpExt != nil && huaweiAdsImpExt.IsTestAuthorization == "true" { 312 isTestAuthorization = true 313 } 314 header = getHeaders(huaweiAdsImpExt, openRTBRequest, isTestAuthorization) 315 bidRequest := &adapters.RequestData{ 316 Method: http.MethodPost, 317 Uri: getFinalEndPoint(countryCode, a.endpoint, a.extraInfo), 318 Body: reqJSON, 319 Headers: header, 320 ImpIDs: openrtb_ext.GetImpIDs(openRTBRequest.Imp), 321 } 322 323 return []*adapters.RequestData{bidRequest}, nil 324 } 325 326 // countryCode is alpha2, choose the corresponding site end point 327 func getFinalEndPoint(countryCode string, defaultEndpoint string, extraInfo ExtraInfo) string { 328 // closeSiteSelectionByCountry == 1, close site selection, use the defaultEndpoint 329 if "1" == extraInfo.CloseSiteSelectionByCountry { 330 return defaultEndpoint 331 } 332 333 if countryCode == "" || len(countryCode) > 2 { 334 return defaultEndpoint 335 } 336 var europeanSiteCountryCodeGroup = map[string]empty{"AX": {}, "AL": {}, "AD": {}, "AU": {}, "AT": {}, "BE": {}, 337 "BA": {}, "BG": {}, "CA": {}, "HR": {}, "CY": {}, "CZ": {}, "DK": {}, "EE": {}, "FO": {}, "FI": {}, 338 "FR": {}, "DE": {}, "GI": {}, "GR": {}, "GL": {}, "GG": {}, "VA": {}, "HU": {}, "IS": {}, "IE": {}, 339 "IM": {}, "IL": {}, "IT": {}, "JE": {}, "YK": {}, "LV": {}, "LI": {}, "LT": {}, "LU": {}, "MT": {}, 340 "MD": {}, "MC": {}, "ME": {}, "NL": {}, "AN": {}, "NZ": {}, "NO": {}, "PL": {}, "PT": {}, "RO": {}, 341 "MF": {}, "VC": {}, "SM": {}, "RS": {}, "SX": {}, "SK": {}, "SI": {}, "ES": {}, "SE": {}, "CH": {}, 342 "TR": {}, "UA": {}, "GB": {}, "US": {}, "MK": {}, "SJ": {}, "BQ": {}, "PM": {}, "CW": {}} 343 var russianSiteCountryCodeGroup = map[string]empty{"RU": {}} 344 var chineseSiteCountryCodeGroup = map[string]empty{"CN": {}} 345 // choose site 346 if _, exists := chineseSiteCountryCodeGroup[countryCode]; exists { 347 return chineseSiteEndPoint 348 } else if _, exists := russianSiteCountryCodeGroup[countryCode]; exists { 349 return russianSiteEndPoint 350 } else if _, exists := europeanSiteCountryCodeGroup[countryCode]; exists { 351 return europeanSiteEndPoint 352 } else { 353 return asianSiteEndPoint 354 } 355 } 356 357 func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, 358 bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) { 359 httpStatusError := checkRespStatusCode(bidderRawResponse) 360 if httpStatusError != nil { 361 return nil, []error{httpStatusError} 362 } 363 364 var huaweiAdsResponse huaweiAdsResponse 365 if err := json.Unmarshal(bidderRawResponse.Body, &huaweiAdsResponse); err != nil { 366 return nil, []error{&errortypes.BadServerResponse{ 367 Message: "Unable to parse server response", 368 }} 369 } 370 371 if err := checkHuaweiAdsResponseRetcode(huaweiAdsResponse); err != nil { 372 return nil, []error{err} 373 } 374 375 bidderResponse, err := a.convertHuaweiAdsRespToBidderResp(&huaweiAdsResponse, openRTBRequest) 376 if err != nil { 377 return nil, []error{err} 378 } 379 380 return bidderResponse, nil 381 } 382 383 // Builder builds a new instance of the HuaweiAds adapter for the given bidder with the given config. 384 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 385 extraInfo, err := getExtraInfo(config.ExtraAdapterInfo) 386 if err != nil { 387 return nil, err 388 } 389 390 bidder := &adapter{ 391 endpoint: config.Endpoint, 392 extraInfo: extraInfo, 393 } 394 return bidder, nil 395 } 396 397 func getExtraInfo(v string) (ExtraInfo, error) { 398 var extraInfo ExtraInfo 399 if len(v) == 0 { 400 return extraInfo, nil 401 } 402 403 if err := json.Unmarshal([]byte(v), &extraInfo); err != nil { 404 return extraInfo, fmt.Errorf("invalid extra info: %v , pls check", err) 405 } 406 407 for _, convert := range extraInfo.PkgNameConvert { 408 if convert.ConvertedPkgName == "" { 409 return extraInfo, fmt.Errorf("invalid extra info: ConvertedPkgName is empty, pls check") 410 } 411 412 if convert.UnconvertedPkgNameKeyWords != nil { 413 for _, keyword := range convert.UnconvertedPkgNameKeyWords { 414 if keyword == "" { 415 return extraInfo, fmt.Errorf("invalid extra info: UnconvertedPkgNameKeyWords has a empty keyword, pls check") 416 } 417 } 418 } 419 420 if convert.UnconvertedPkgNamePrefixs != nil { 421 for _, prefix := range convert.UnconvertedPkgNamePrefixs { 422 if prefix == "" { 423 return extraInfo, fmt.Errorf("invalid extra info: UnconvertedPkgNamePrefixs has a empty value, pls check") 424 } 425 } 426 } 427 } 428 return extraInfo, nil 429 } 430 431 // getHeaders: get request header, Authorization -> digest 432 func getHeaders(huaweiAdsImpExt *openrtb_ext.ExtImpHuaweiAds, request *openrtb2.BidRequest, isTestAuthorization bool) http.Header { 433 headers := http.Header{} 434 headers.Add("Content-Type", "application/json;charset=utf-8") 435 headers.Add("Accept", "application/json") 436 if huaweiAdsImpExt == nil { 437 return headers 438 } 439 headers.Add("Authorization", getDigestAuthorization(huaweiAdsImpExt, isTestAuthorization)) 440 441 if request.Device != nil && len(request.Device.UA) > 0 { 442 headers.Add("User-Agent", request.Device.UA) 443 } 444 return headers 445 } 446 447 // getReqJson: get body json for HuaweiAds request 448 func getReqJson(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest, extraInfo ExtraInfo) (countryCode string, err error) { 449 request.Version = huaweiAdxApiVersion 450 if countryCode, err = getReqAppInfo(request, openRTBRequest, extraInfo); err != nil { 451 return "", err 452 } 453 if err = getReqDeviceInfo(request, openRTBRequest); err != nil { 454 return "", err 455 } 456 getReqNetWorkInfo(request, openRTBRequest) 457 getReqRegsInfo(request, openRTBRequest) 458 getReqGeoInfo(request, openRTBRequest) 459 getReqConsentInfo(request, openRTBRequest) 460 return countryCode, nil 461 } 462 463 func getReqAdslot30(huaweiAdsImpExt *openrtb_ext.ExtImpHuaweiAds, 464 openRTBImp *openrtb2.Imp) (adslot30, error) { 465 adtype := convertAdtypeStringToInteger(strings.ToLower(huaweiAdsImpExt.Adtype)) 466 testStatus := 0 467 if huaweiAdsImpExt.IsTestAuthorization == "true" { 468 testStatus = 1 469 } 470 var adslot30 = adslot30{ 471 Slotid: huaweiAdsImpExt.SlotId, 472 Adtype: adtype, 473 Test: int32(testStatus), 474 } 475 if err := checkAndExtractOpenrtbFormat(&adslot30, adtype, huaweiAdsImpExt.Adtype, openRTBImp); err != nil { 476 return adslot30, err 477 } 478 return adslot30, nil 479 } 480 481 // opentrb : huawei adtype 482 // banner <-> banner, interstitial 483 // native <-> native 484 // video <-> banner, roll, interstitial, rewarded 485 func checkAndExtractOpenrtbFormat(adslot30 *adslot30, adtype int32, yourAdtype string, openRTBImp *openrtb2.Imp) error { 486 if openRTBImp.Banner != nil { 487 if adtype != banner && adtype != interstitial { 488 return errors.New("check openrtb format: request has banner, doesn't correspond to huawei adtype " + yourAdtype) 489 } 490 getBannerFormat(adslot30, openRTBImp) 491 } else if openRTBImp.Native != nil { 492 if adtype != native { 493 return errors.New("check openrtb format: request has native, doesn't correspond to huawei adtype " + yourAdtype) 494 } 495 if err := getNativeFormat(adslot30, openRTBImp); err != nil { 496 return err 497 } 498 } else if openRTBImp.Video != nil { 499 if adtype != banner && adtype != interstitial && adtype != rewarded && adtype != roll { 500 return errors.New("check openrtb format: request has video, doesn't correspond to huawei adtype " + yourAdtype) 501 } 502 if err := getVideoFormat(adslot30, adtype, openRTBImp); err != nil { 503 return err 504 } 505 } else if openRTBImp.Audio != nil { 506 return errors.New("check openrtb format: request has audio, not currently supported") 507 } else { 508 return errors.New("check openrtb format: please choose one of our supported type banner, native, or video") 509 } 510 return nil 511 } 512 513 func getBannerFormat(adslot30 *adslot30, openRTBImp *openrtb2.Imp) { 514 if openRTBImp.Banner.W != nil && openRTBImp.Banner.H != nil { 515 adslot30.W = *openRTBImp.Banner.W 516 adslot30.H = *openRTBImp.Banner.H 517 } 518 if len(openRTBImp.Banner.Format) != 0 { 519 var formats = make([]format, 0, len(openRTBImp.Banner.Format)) 520 for _, f := range openRTBImp.Banner.Format { 521 if f.H != 0 && f.W != 0 { 522 formats = append(formats, format{f.W, f.H}) 523 } 524 } 525 adslot30.Format = formats 526 } 527 } 528 529 func getNativeFormat(adslot30 *adslot30, openRTBImp *openrtb2.Imp) error { 530 if openRTBImp.Native.Request == "" { 531 return errors.New("extract openrtb native failed: imp.Native.Request is empty") 532 } 533 534 var nativePayload nativeRequests.Request 535 if err := json.Unmarshal(json.RawMessage(openRTBImp.Native.Request), &nativePayload); err != nil { 536 return err 537 } 538 539 //popular size for native ads 540 popularSizes := []format{{W: 225, H: 150}, {W: 1080, H: 607}, {W: 300, H: 250}, {W: 1080, H: 1620}, {W: 1280, H: 720}, {W: 640, H: 360}, {W: 1080, H: 1920}, {W: 720, H: 1280}} 541 542 // only compute the main image number, type = native1.ImageAssetTypeMain 543 var numMainImage = 0 544 var numVideo = 0 545 var formats = make([]format, 0) 546 var numFormat = 0 547 var detailedCreativeTypeList = make([]string, 0, 2) 548 549 //number of the requested image size 550 for _, asset := range nativePayload.Assets { 551 if numFormat > 1 { 552 break 553 } 554 if asset.Img != nil { 555 if asset.Img.Type == native1.ImageAssetTypeMain { 556 numFormat++ 557 } 558 } 559 } 560 561 sizeMap := make(map[format]struct{}) 562 for _, size := range popularSizes { 563 sizeMap[size] = struct{}{} 564 } 565 566 for _, asset := range nativePayload.Assets { 567 // Only one of the {title,img,video,data} objects should be present in each object. 568 if asset.Video != nil { 569 numVideo++ 570 formats = popularSizes 571 572 w := ptrutil.ValueOrDefault(asset.Video.W) 573 h := ptrutil.ValueOrDefault(asset.Video.H) 574 575 _, ok := sizeMap[format{W: w, H: h}] 576 if (w != 0 && h != 0) && !ok { 577 formats = append(formats, format{w, h}) 578 } 579 } 580 // every image has the same W, H. 581 if asset.Img != nil { 582 if asset.Img.Type == native1.ImageAssetTypeMain { 583 numMainImage++ 584 if numFormat > 1 && asset.Img.H != 0 && asset.Img.W != 0 && asset.Img.WMin != 0 && asset.Img.HMin != 0 { 585 formats = append(formats, format{asset.Img.W, asset.Img.H}) 586 } 587 if numFormat == 1 && asset.Img.H != 0 && asset.Img.W != 0 && asset.Img.WMin != 0 && asset.Img.HMin != 0 { 588 result := filterPopularSizes(popularSizes, asset.Img.W, asset.Img.H, "ratio") 589 formats = append(formats, result...) 590 } 591 if numFormat == 1 && asset.Img.H == 0 && asset.Img.W == 0 && asset.Img.WMin != 0 && asset.Img.HMin != 0 { 592 result := filterPopularSizes(popularSizes, asset.Img.WMin, asset.Img.HMin, "range") 593 formats = append(formats, result...) 594 } 595 } 596 } 597 adslot30.Format = formats 598 } 599 if numVideo >= 1 { 600 detailedCreativeTypeList = append(detailedCreativeTypeList, "903") 601 } 602 if numMainImage >= 1 { 603 detailedCreativeTypeList = append(detailedCreativeTypeList, "901", "904", "905") 604 } 605 adslot30.DetailedCreativeTypeList = detailedCreativeTypeList 606 return nil 607 } 608 609 // filter popular size by range or ratio to append format array 610 func filterPopularSizes(sizes []format, width int64, height int64, byWhat string) []format { 611 612 filtered := []format{} 613 for _, size := range sizes { 614 w := size.W 615 h := size.H 616 617 if byWhat == "ratio" { 618 ratio := float64(width) / float64(height) 619 diff := math.Abs(float64(w)/float64(h) - ratio) 620 if diff <= 0.5 { 621 filtered = append(filtered, size) 622 } 623 } 624 if byWhat == "range" && w > width && h > height { 625 filtered = append(filtered, size) 626 } 627 } 628 return filtered 629 } 630 631 // roll ad need TotalDuration 632 func getVideoFormat(adslot30 *adslot30, adtype int32, openRTBImp *openrtb2.Imp) error { 633 adslot30.W = ptrutil.ValueOrDefault(openRTBImp.Video.W) 634 adslot30.H = ptrutil.ValueOrDefault(openRTBImp.Video.H) 635 636 if adtype == roll { 637 if openRTBImp.Video.MaxDuration == 0 { 638 return errors.New("extract openrtb video failed: MaxDuration is empty when huaweiads adtype is roll.") 639 } 640 adslot30.TotalDuration = int32(openRTBImp.Video.MaxDuration) 641 } 642 return nil 643 } 644 645 func convertAdtypeStringToInteger(adtypeLower string) int32 { 646 switch adtypeLower { 647 case "banner": 648 return banner 649 case "native": 650 return native 651 case "rewarded": 652 return rewarded 653 case "interstitial": 654 return interstitial 655 case "roll": 656 return roll 657 case "splash": 658 return splash 659 case "magazinelock": 660 return magazinelock 661 case "audio": 662 return audio 663 default: 664 return banner 665 } 666 } 667 668 // getReqAppInfo: get app information for HuaweiAds request 669 func getReqAppInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest, extraInfo ExtraInfo) (countryCode string, err error) { 670 var app app 671 if openRTBRequest.App != nil { 672 if openRTBRequest.App.Ver != "" { 673 app.Version = openRTBRequest.App.Ver 674 } 675 if openRTBRequest.App.Name != "" { 676 app.Name = openRTBRequest.App.Name 677 } 678 679 // bundle cannot be empty, we need package name. 680 if openRTBRequest.App.Bundle != "" { 681 app.Pkgname = getFinalPkgName(openRTBRequest.App.Bundle, extraInfo) 682 } else { 683 return "", errors.New("generate HuaweiAds AppInfo failed: openrtb BidRequest.App.Bundle is empty.") 684 } 685 686 if openRTBRequest.App.Content != nil && openRTBRequest.App.Content.Language != "" { 687 app.Lang = openRTBRequest.App.Content.Language 688 } else { 689 app.Lang = "en" 690 } 691 } 692 countryCode = getCountryCode(openRTBRequest) 693 app.Country = countryCode 694 request.App = app 695 return countryCode, nil 696 } 697 698 // when it has pkgNameConvert (include different rules) 699 // 1. when bundleName in ExceptionPkgNames, finalPkgname = bundleName 700 // 2. when bundleName conform UnconvertedPkgNames, finalPkgname = ConvertedPkgName 701 // 3. when bundleName conform keyword, finalPkgname = ConvertedPkgName 702 // 4. when bundleName conform prefix, finalPkgname = ConvertedPkgName 703 func getFinalPkgName(bundleName string, extraInfo ExtraInfo) string { 704 for _, convert := range extraInfo.PkgNameConvert { 705 if convert.ConvertedPkgName == "" { 706 continue 707 } 708 709 for _, name := range convert.ExceptionPkgNames { 710 if name == bundleName { 711 return bundleName 712 } 713 } 714 715 for _, name := range convert.UnconvertedPkgNames { 716 if name == bundleName || name == "*" { 717 return convert.ConvertedPkgName 718 } 719 } 720 721 for _, keyword := range convert.UnconvertedPkgNameKeyWords { 722 if strings.Index(bundleName, keyword) > 0 { 723 return convert.ConvertedPkgName 724 } 725 } 726 727 for _, prefix := range convert.UnconvertedPkgNamePrefixs { 728 if strings.HasPrefix(bundleName, prefix) { 729 return convert.ConvertedPkgName 730 } 731 } 732 } 733 return bundleName 734 } 735 736 // getClientTime: get field clientTime, format: 2006-01-02 15:04:05.000+0200 737 // If this parameter is not passed, the server time is used 738 func getClientTime(clientTime string) (newClientTime string) { 739 var zone = defaultTimeZone 740 t := time.Now().Local().Format(time.RFC822Z) 741 index := strings.IndexAny(t, "-+") 742 if index > 0 && len(t)-index == 5 { 743 zone = t[index:] 744 } 745 if clientTime == "" { 746 return time.Now().Format(timeFormat) + zone 747 } 748 if isMatched, _ := regexp.MatchString("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]{1}\\d{4}$", clientTime); isMatched { 749 return clientTime 750 } 751 if isMatched, _ := regexp.MatchString("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}$", clientTime); isMatched { 752 return clientTime + zone 753 } 754 return time.Now().Format(timeFormat) + zone 755 } 756 757 // getReqDeviceInfo: get device information for HuaweiAds request 758 func getReqDeviceInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest) (err error) { 759 var device device 760 if openRTBRequest.Device != nil { 761 device.Type = int32(openRTBRequest.Device.DeviceType) 762 device.Useragent = openRTBRequest.Device.UA 763 device.Os = openRTBRequest.Device.OS 764 device.Version = openRTBRequest.Device.OSV 765 device.Maker = openRTBRequest.Device.Make 766 device.Model = openRTBRequest.Device.Model 767 if device.Model == "" { 768 device.Model = defaultModelName 769 } 770 device.Height = int32(openRTBRequest.Device.H) 771 device.Width = int32(openRTBRequest.Device.W) 772 device.Language = openRTBRequest.Device.Language 773 device.Pxratio = float32(openRTBRequest.Device.PxRatio) 774 var country = getCountryCode(openRTBRequest) 775 device.BelongCountry = country 776 device.LocaleCountry = country 777 device.Ip = openRTBRequest.Device.IP 778 device.Gaid = openRTBRequest.Device.IFA 779 device.ClientTime = getClientTime("") 780 } 781 782 // get oaid gaid imei in openRTBRequest.User.Ext.Data 783 if err = getDeviceIDFromUserExt(&device, openRTBRequest); err != nil { 784 return err 785 } 786 787 // IsTrackingEnabled = 1 - DNT 788 if openRTBRequest.Device != nil && openRTBRequest.Device.DNT != nil { 789 if device.Oaid != "" { 790 device.IsTrackingEnabled = strconv.Itoa(1 - int(*openRTBRequest.Device.DNT)) 791 } 792 if device.Gaid != "" { 793 device.GaidTrackingEnabled = strconv.Itoa(1 - int(*openRTBRequest.Device.DNT)) 794 } 795 } 796 797 request.Device = device 798 return nil 799 } 800 801 func getCountryCode(openRTBRequest *openrtb2.BidRequest) string { 802 if openRTBRequest.Device != nil && openRTBRequest.Device.Geo != nil && openRTBRequest.Device.Geo.Country != "" { 803 return convertCountryCode(openRTBRequest.Device.Geo.Country) 804 } else if openRTBRequest.User != nil && openRTBRequest.User.Geo != nil && openRTBRequest.User.Geo.Country != "" { 805 return convertCountryCode(openRTBRequest.User.Geo.Country) 806 } else if openRTBRequest.Device != nil && openRTBRequest.Device.MCCMNC != "" { 807 return getCountryCodeFromMCC(openRTBRequest.Device.MCCMNC) 808 } else { 809 return defaultCountryName 810 } 811 } 812 813 // convertCountryCode: ISO 3166-1 Alpha3 -> Alpha2, Some countries may use 814 func convertCountryCode(country string) (out string) { 815 if country == "" { 816 return defaultCountryName 817 } 818 var mapCountryCodeAlpha3ToAlpha2 = map[string]string{"AND": "AD", "AGO": "AO", "AUT": "AT", "BGD": "BD", 819 "BLR": "BY", "CAF": "CF", "TCD": "TD", "CHL": "CL", "CHN": "CN", "COG": "CG", "COD": "CD", "DNK": "DK", 820 "GNQ": "GQ", "EST": "EE", "GIN": "GN", "GNB": "GW", "GUY": "GY", "IRQ": "IQ", "IRL": "IE", "ISR": "IL", 821 "KAZ": "KZ", "LBY": "LY", "MDG": "MG", "MDV": "MV", "MEX": "MX", "MNE": "ME", "MOZ": "MZ", "PAK": "PK", 822 "PNG": "PG", "PRY": "PY", "POL": "PL", "PRT": "PT", "SRB": "RS", "SVK": "SK", "SVN": "SI", "SWE": "SE", 823 "TUN": "TN", "TUR": "TR", "TKM": "TM", "UKR": "UA", "ARE": "AE", "URY": "UY"} 824 if mappedCountry, exists := mapCountryCodeAlpha3ToAlpha2[country]; exists { 825 return mappedCountry 826 } 827 828 if len(country) >= 2 { 829 return country[0:2] 830 } 831 832 return defaultCountryName 833 } 834 835 func getCountryCodeFromMCC(MCC string) (out string) { 836 var countryMCC = strings.Split(MCC, "-")[0] 837 intVar, err := strconv.Atoi(countryMCC) 838 839 if err != nil { 840 return defaultCountryName 841 } else { 842 if result, found := MccList[intVar]; found { 843 return strings.ToUpper(result) 844 } else { 845 return defaultCountryName 846 } 847 } 848 } 849 850 // getDeviceID include oaid gaid imei. In prebid mobile, use TargetingParams.addUserData("imei", "imei-test"); 851 // When ifa: gaid exists, other device id can be passed by TargetingParams.addUserData("oaid", "oaid-test"); 852 func getDeviceIDFromUserExt(device *device, openRTBRequest *openrtb2.BidRequest) (err error) { 853 var userObjExist = true 854 if openRTBRequest.User == nil || openRTBRequest.User.Ext == nil { 855 userObjExist = false 856 } 857 if userObjExist { 858 var extUserDataHuaweiAds openrtb_ext.ExtUserDataHuaweiAds 859 if err := json.Unmarshal(openRTBRequest.User.Ext, &extUserDataHuaweiAds); err != nil { 860 return errors.New("get gaid from openrtb Device.IFA failed, and get device id failed: Unmarshal openRTBRequest.User.Ext -> extUserDataHuaweiAds. Error: " + err.Error()) 861 } 862 863 var deviceId = extUserDataHuaweiAds.Data 864 isValidDeviceId := false 865 866 if len(deviceId.Oaid) > 0 { 867 device.Oaid = deviceId.Oaid[0] 868 isValidDeviceId = true 869 } 870 if len(deviceId.Gaid) > 0 { 871 device.Gaid = deviceId.Gaid[0] 872 isValidDeviceId = true 873 } 874 if len(device.Gaid) > 0 { 875 isValidDeviceId = true 876 } 877 if len(deviceId.Imei) > 0 { 878 device.Imei = deviceId.Imei[0] 879 isValidDeviceId = true 880 } 881 882 if !isValidDeviceId { 883 return errors.New("getDeviceID: Imei ,Oaid, Gaid are all empty.") 884 } 885 if len(deviceId.ClientTime) > 0 { 886 device.ClientTime = getClientTime(deviceId.ClientTime[0]) 887 } 888 } else { 889 if len(device.Gaid) == 0 { 890 return errors.New("getDeviceID: openRTBRequest.User.Ext is nil and device.Gaid is not specified.") 891 } 892 } 893 return nil 894 } 895 896 // getReqNetWorkInfo: for HuaweiAds request, include Carrier, Mcc, Mnc 897 func getReqNetWorkInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest) { 898 if openRTBRequest.Device != nil { 899 var network network 900 if openRTBRequest.Device.ConnectionType != nil { 901 network.Type = int32(*openRTBRequest.Device.ConnectionType) 902 } else { 903 network.Type = defaultUnknownNetworkType 904 } 905 906 var cellInfos []cellInfo 907 if openRTBRequest.Device.MCCMNC != "" { 908 var arr = strings.Split(openRTBRequest.Device.MCCMNC, "-") 909 network.Carrier = 0 910 if len(arr) >= 2 { 911 cellInfos = append(cellInfos, cellInfo{ 912 Mcc: arr[0], 913 Mnc: arr[1], 914 }) 915 var str = arr[0] + arr[1] 916 if str == "46000" || str == "46002" || str == "46007" { 917 network.Carrier = 2 918 } else if str == "46001" || str == "46006" { 919 network.Carrier = 1 920 } else if str == "46003" || str == "46005" || str == "46011" { 921 network.Carrier = 3 922 } else { 923 network.Carrier = 99 924 } 925 } 926 } 927 network.CellInfo = cellInfos 928 request.Network = network 929 } 930 } 931 932 // getReqRegsInfo: get regs information for HuaweiAds request, include Coppa 933 func getReqRegsInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest) { 934 if openRTBRequest.Regs != nil && openRTBRequest.Regs.COPPA >= 0 { 935 var regs regs 936 regs.Coppa = int32(openRTBRequest.Regs.COPPA) 937 request.Regs = regs 938 } 939 } 940 941 // getReqGeoInfo: get geo information for HuaweiAds request, include Lon, Lat, Accuracy, Lastfix 942 func getReqGeoInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest) { 943 if openRTBRequest.Device != nil && openRTBRequest.Device.Geo != nil { 944 request.Geo = geo{ 945 Lon: float32(ptrutil.ValueOrDefault(openRTBRequest.Device.Geo.Lon)), 946 Lat: float32(ptrutil.ValueOrDefault(openRTBRequest.Device.Geo.Lat)), 947 Accuracy: int32(openRTBRequest.Device.Geo.Accuracy), 948 Lastfix: int32(openRTBRequest.Device.Geo.LastFix), 949 } 950 } 951 } 952 953 // getReqGeoInfo: get GDPR consent 954 func getReqConsentInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRequest) { 955 if openRTBRequest.User != nil && openRTBRequest.User.Ext != nil { 956 var extUser openrtb_ext.ExtUser 957 if err := json.Unmarshal(openRTBRequest.User.Ext, &extUser); err != nil { 958 return 959 } 960 request.Consent = extUser.Consent 961 } 962 } 963 964 func unmarshalExtImpHuaweiAds(openRTBImp *openrtb2.Imp) (*openrtb_ext.ExtImpHuaweiAds, error) { 965 var bidderExt adapters.ExtImpBidder 966 var huaweiAdsImpExt openrtb_ext.ExtImpHuaweiAds 967 if err := json.Unmarshal(openRTBImp.Ext, &bidderExt); err != nil { 968 return nil, errors.New("Unmarshal: openRTBImp.Ext -> bidderExt failed") 969 } 970 if err := json.Unmarshal(bidderExt.Bidder, &huaweiAdsImpExt); err != nil { 971 return nil, errors.New("Unmarshal: bidderExt.Bidder -> huaweiAdsImpExt failed") 972 } 973 if huaweiAdsImpExt.SlotId == "" { 974 return nil, errors.New("ExtImpHuaweiAds: slotid is empty.") 975 } 976 if huaweiAdsImpExt.Adtype == "" { 977 return nil, errors.New("ExtImpHuaweiAds: adtype is empty.") 978 } 979 if huaweiAdsImpExt.PublisherId == "" { 980 return nil, errors.New("ExtHuaweiAds: publisherid is empty.") 981 } 982 if huaweiAdsImpExt.SignKey == "" { 983 return nil, errors.New("ExtHuaweiAds: signkey is empty.") 984 } 985 if huaweiAdsImpExt.KeyId == "" { 986 return nil, errors.New("ExtImpHuaweiAds: keyid is empty.") 987 } 988 return &huaweiAdsImpExt, nil 989 } 990 991 func checkRespStatusCode(response *adapters.ResponseData) error { 992 if response.StatusCode == http.StatusNoContent { 993 return nil 994 } 995 996 if response.StatusCode == http.StatusServiceUnavailable { 997 return &errortypes.BadInput{ 998 Message: fmt.Sprintf("Something went wrong, please contact your Account Manager. Status Code: [ %d ] ", response.StatusCode), 999 } 1000 } 1001 1002 if response.StatusCode != http.StatusOK { 1003 return &errortypes.BadInput{ 1004 Message: fmt.Sprintf("Unexpected status code: [ %d ]. Run with request.debug = 1 for more info", response.StatusCode), 1005 } 1006 } 1007 1008 if response.Body == nil { 1009 return errors.New("bidderRawResponse body is empty") 1010 } 1011 return nil 1012 } 1013 1014 func checkHuaweiAdsResponseRetcode(response huaweiAdsResponse) error { 1015 if response.Retcode == 200 || response.Retcode == 206 { 1016 return nil 1017 } 1018 if response.Retcode == 204 { 1019 return &errortypes.BadInput{ 1020 Message: fmt.Sprintf("HuaweiAdsResponse retcode: %d , reason: The request packet is correct, but no advertisement was found for this request.", response.Retcode), 1021 } 1022 } 1023 if (response.Retcode < 600 && response.Retcode >= 400) || (response.Retcode < 300 && response.Retcode > 200) { 1024 return &errortypes.BadInput{ 1025 Message: fmt.Sprintf("HuaweiAdsResponse retcode: %d , reason: %s", response.Retcode, response.Reason), 1026 } 1027 } 1028 return nil 1029 } 1030 1031 // convertHuaweiAdsRespToBidderResp: convert HuaweiAds' response into bidder's response 1032 func (a *adapter) convertHuaweiAdsRespToBidderResp(huaweiAdsResponse *huaweiAdsResponse, openRTBRequest *openrtb2.BidRequest) (bidderResponse *adapters.BidderResponse, err error) { 1033 if len(huaweiAdsResponse.Multiad) == 0 { 1034 return nil, errors.New("convert huaweiads response to bidder response failed: multiad length is 0, get no ads from huawei side.") 1035 } 1036 bidderResponse = adapters.NewBidderResponseWithBidsCapacity(len(huaweiAdsResponse.Multiad)) 1037 // Default Currency: CNY 1038 bidderResponse.Currency = "CNY" 1039 1040 // record request Imp (slotid->imp, slotid->openrtb_ext.bidtype) 1041 mapSlotid2Imp := make(map[string]openrtb2.Imp, len(openRTBRequest.Imp)) 1042 mapSlotid2MediaType := make(map[string]openrtb_ext.BidType, len(openRTBRequest.Imp)) 1043 for _, imp := range openRTBRequest.Imp { 1044 huaweiAdsExt, err := unmarshalExtImpHuaweiAds(&imp) 1045 if err != nil { 1046 continue 1047 } 1048 mapSlotid2Imp[huaweiAdsExt.SlotId] = imp 1049 1050 var mediaType = openrtb_ext.BidTypeBanner 1051 if imp.Video != nil { 1052 mediaType = openrtb_ext.BidTypeVideo 1053 } else if imp.Native != nil { 1054 mediaType = openrtb_ext.BidTypeNative 1055 } else if imp.Audio != nil { 1056 mediaType = openrtb_ext.BidTypeAudio 1057 } 1058 mapSlotid2MediaType[huaweiAdsExt.SlotId] = mediaType 1059 } 1060 1061 if len(mapSlotid2MediaType) < 1 || len(mapSlotid2Imp) < 1 { 1062 return nil, errors.New("convert huaweiads response to bidder response failed: openRTBRequest.imp is nil") 1063 } 1064 1065 for _, ad30 := range huaweiAdsResponse.Multiad { 1066 if mapSlotid2Imp[ad30.Slotid].ID == "" { 1067 continue 1068 } 1069 1070 if ad30.Retcode30 != 200 { 1071 continue 1072 } 1073 1074 for _, content := range ad30.Content { 1075 var bid openrtb2.Bid 1076 bid.ID = mapSlotid2Imp[ad30.Slotid].ID 1077 bid.ImpID = mapSlotid2Imp[ad30.Slotid].ID 1078 // The bidder has already helped us automatically convert the currency price, here only the CNY price is filled in 1079 bid.Price = content.Price 1080 bid.CrID = content.Contentid 1081 // All currencies should be the same 1082 if content.Cur != "" { 1083 bidderResponse.Currency = content.Cur 1084 } 1085 1086 bid.AdM, bid.W, bid.H, err = a.handleHuaweiAdsContent(ad30.AdType, &content, mapSlotid2MediaType[ad30.Slotid], mapSlotid2Imp[ad30.Slotid]) 1087 if err != nil { 1088 return nil, err 1089 } 1090 bid.ADomain = append(bid.ADomain, "huaweiads") 1091 bid.NURL = getNurl(content) 1092 bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ 1093 Bid: &bid, 1094 BidType: mapSlotid2MediaType[ad30.Slotid], 1095 }) 1096 } 1097 } 1098 return bidderResponse, nil 1099 } 1100 1101 func getNurl(content content) string { 1102 if len(content.Monitor) == 0 { 1103 return "" 1104 } 1105 for _, monitor := range content.Monitor { 1106 if monitor.EventType == "win" && len(monitor.Url) != 0 { 1107 return monitor.Url[0] 1108 } 1109 } 1110 return "" 1111 } 1112 1113 // handleHuaweiAdsContent: get field Adm, Width, Height 1114 func (a *adapter) handleHuaweiAdsContent(adType int32, content *content, bidType openrtb_ext.BidType, imp openrtb2.Imp) ( 1115 adm string, adWidth int64, adHeight int64, err error) { 1116 switch bidType { 1117 case openrtb_ext.BidTypeBanner: 1118 adm, adWidth, adHeight, err = a.extractAdmBanner(adType, content, bidType, imp) 1119 case openrtb_ext.BidTypeNative: 1120 adm, adWidth, adHeight, err = a.extractAdmNative(adType, content, bidType, imp) 1121 case openrtb_ext.BidTypeVideo: 1122 adm, adWidth, adHeight, err = a.extractAdmVideo(adType, content, bidType, imp) 1123 default: 1124 return "", 0, 0, errors.New("no support bidtype: audio") 1125 } 1126 1127 if err != nil { 1128 return "", 0, 0, fmt.Errorf("generate Adm field from HuaweiAds response failed: %s", err) 1129 } 1130 return adm, adWidth, adHeight, nil 1131 } 1132 1133 // extractAdmBanner: banner ad 1134 func (a *adapter) extractAdmBanner(adType int32, content *content, bidType openrtb_ext.BidType, imp openrtb2.Imp) (adm string, 1135 adWidth int64, adHeight int64, err error) { 1136 // support openrtb: banner <=> huawei adtype: banner, interstitial 1137 if adType != banner && adType != interstitial { 1138 return "", 0, 0, errors.New("openrtb banner should correspond to huaweiads adtype: banner or interstitial") 1139 } 1140 var creativeType = content.Creativetype 1141 if content.Creativetype > 100 { 1142 creativeType = creativeType - 100 1143 } 1144 if creativeType == text || creativeType == bigPicture || creativeType == bigPicture2 || 1145 creativeType == smallPicture || creativeType == threeSmallPicturesText || 1146 creativeType == iconText || creativeType == gif { 1147 return a.extractAdmPicture(content) 1148 } else if creativeType == videoText || creativeType == video || creativeType == videoWithPicturesText { 1149 return a.extractAdmVideo(adType, content, bidType, imp) 1150 } else { 1151 return "", 0, 0, errors.New("no banner support creativetype") 1152 } 1153 } 1154 1155 // extractAdmNative: native ad 1156 func (a *adapter) extractAdmNative(adType int32, content *content, bidType openrtb_ext.BidType, openrtb2Imp openrtb2.Imp) (adm string, 1157 adWidth int64, adHeight int64, err error) { 1158 if adType != native { 1159 return "", 0, 0, errors.New("extract Adm for Native ad: huaweiads response is not a native ad") 1160 } 1161 if openrtb2Imp.Native == nil { 1162 return "", 0, 0, errors.New("extract Adm for Native ad: imp.Native is nil") 1163 } 1164 if openrtb2Imp.Native.Request == "" { 1165 return "", 0, 0, errors.New("extract Adm for Native ad: imp.Native.Request is empty") 1166 } 1167 1168 var nativePayload nativeRequests.Request 1169 if err := json.Unmarshal(json.RawMessage(openrtb2Imp.Native.Request), &nativePayload); err != nil { 1170 return "", 0, 0, err 1171 } 1172 1173 var nativeResult nativeResponse.Response 1174 var linkObject nativeResponse.Link 1175 linkObject.URL, err = a.getClickUrl(content) 1176 if err != nil { 1177 return "", 0, 0, err 1178 } 1179 1180 nativeResult.Assets = make([]nativeResponse.Asset, 0, len(nativePayload.Assets)) 1181 var imgIndex = 0 1182 var iconIndex = 0 1183 for _, asset := range nativePayload.Assets { 1184 var responseAsset nativeResponse.Asset 1185 if asset.Title != nil { 1186 var titleObject nativeResponse.Title 1187 titleObject.Text = getDecodeValue(content.MetaData.Title) 1188 titleObject.Len = int64(len(titleObject.Text)) 1189 responseAsset.Title = &titleObject 1190 } else if asset.Video != nil { 1191 var videoObject nativeResponse.Video 1192 var err error 1193 if videoObject.VASTTag, adWidth, adHeight, err = a.extractAdmVideo(adType, content, bidType, openrtb2Imp); err != nil { 1194 return "", 0, 0, err 1195 } 1196 responseAsset.Video = &videoObject 1197 } else if asset.Img != nil { 1198 if len(content.MetaData.ImageInfo) == imgIndex && asset.Img.Type == native1.ImageAssetTypeMain { 1199 continue 1200 } 1201 var imgObject nativeResponse.Image 1202 imgObject.URL = "" 1203 imgObject.Type = asset.Img.Type 1204 if asset.Img.Type == native1.ImageAssetTypeIcon { 1205 if len(content.MetaData.Icon) > iconIndex { 1206 imgObject.URL = content.MetaData.Icon[iconIndex].Url 1207 imgObject.W = content.MetaData.Icon[iconIndex].Width 1208 imgObject.H = content.MetaData.Icon[iconIndex].Height 1209 iconIndex++ 1210 } 1211 } else { 1212 if len(content.MetaData.ImageInfo) > imgIndex { 1213 imgObject.URL = content.MetaData.ImageInfo[imgIndex].Url 1214 imgObject.W = content.MetaData.ImageInfo[imgIndex].Width 1215 imgObject.H = content.MetaData.ImageInfo[imgIndex].Height 1216 imgIndex++ 1217 } 1218 } 1219 if adHeight == 0 && adWidth == 0 { 1220 adHeight = imgObject.H 1221 adWidth = imgObject.W 1222 } 1223 responseAsset.Img = &imgObject 1224 } else if asset.Data != nil { 1225 var dataObject nativeResponse.Data 1226 dataObject.Label = "" 1227 dataObject.Value = "" 1228 if asset.Data.Type == native1.DataAssetTypeDesc || asset.Data.Type == native1.DataAssetTypeDesc2 { 1229 dataObject.Label = "desc" 1230 dataObject.Value = getDecodeValue(content.MetaData.Description) 1231 } 1232 1233 if asset.Data.Type == native1.DataAssetTypeCTAText { 1234 dataObject.Type = native1.DataAssetTypeCTAText 1235 dataObject.Value = getDecodeValue(content.MetaData.Cta) 1236 } 1237 responseAsset.Data = &dataObject 1238 } 1239 var id = asset.ID 1240 responseAsset.ID = &id 1241 nativeResult.Assets = append(nativeResult.Assets, responseAsset) 1242 } 1243 1244 // dsp imp click tracking + imp click tracking 1245 var eventTrackers []nativeResponse.EventTracker 1246 if content.Monitor != nil { 1247 for _, monitor := range content.Monitor { 1248 if len(monitor.Url) == 0 { 1249 continue 1250 } 1251 if monitor.EventType == "click" { 1252 linkObject.ClickTrackers = append(linkObject.ClickTrackers, monitor.Url...) 1253 } 1254 if monitor.EventType == "imp" { 1255 for i := range monitor.Url { 1256 var eventTracker nativeResponse.EventTracker 1257 eventTracker.Event = native1.EventTypeImpression 1258 eventTracker.Method = native1.EventTrackingMethodImage 1259 eventTracker.URL = monitor.Url[i] 1260 eventTrackers = append(eventTrackers, eventTracker) 1261 } 1262 } 1263 } 1264 } 1265 nativeResult.EventTrackers = eventTrackers 1266 nativeResult.Link = linkObject 1267 nativeResult.Ver = "1.1" 1268 if nativePayload.Ver != "" { 1269 nativeResult.Ver = nativePayload.Ver 1270 } 1271 1272 var result []byte 1273 if result, err = jsonEncode(nativeResult); err != nil { 1274 return "", 0, 0, err 1275 } 1276 return strings.Replace(string(result), "\n", "", -1), adWidth, adHeight, nil 1277 } 1278 1279 func getDecodeValue(str string) string { 1280 if str == "" { 1281 return "" 1282 } 1283 if decodeValue, err := url.QueryUnescape(str); err == nil { 1284 return decodeValue 1285 } else { 1286 return "" 1287 } 1288 } 1289 1290 func jsonEncode(nativeResult nativeResponse.Response) ([]byte, error) { 1291 buffer := &bytes.Buffer{} 1292 encoder := json.NewEncoder(buffer) 1293 encoder.SetEscapeHTML(false) 1294 err := encoder.Encode(nativeResult) 1295 return buffer.Bytes(), err 1296 } 1297 1298 // extractAdmPicture: For banner single picture 1299 func (a *adapter) extractAdmPicture(content *content) (adm string, adWidth int64, adHeight int64, err error) { 1300 if content == nil { 1301 return "", 0, 0, errors.New("extract Adm failed: content is empty") 1302 } 1303 1304 var clickUrl = "" 1305 clickUrl, err = a.getClickUrl(content) 1306 if err != nil { 1307 return "", 0, 0, err 1308 } 1309 1310 var imageInfoUrl string 1311 if content.MetaData.ImageInfo != nil { 1312 imageInfoUrl = content.MetaData.ImageInfo[0].Url 1313 adHeight = content.MetaData.ImageInfo[0].Height 1314 adWidth = content.MetaData.ImageInfo[0].Width 1315 } else { 1316 return "", 0, 0, errors.New("content.MetaData.ImageInfo is empty") 1317 } 1318 1319 var imageTitle = "" 1320 imageTitle = getDecodeValue(content.MetaData.Title) 1321 // dspImp, Imp, dspClick, Click tracking all can be found in MonitorUrl(imp ,click) 1322 dspImpTrackings, dspClickTrackings := getDspImpClickTrackings(content) 1323 var dspImpTrackings2StrImg strings.Builder 1324 for i := 0; i < len(dspImpTrackings); i++ { 1325 dspImpTrackings2StrImg.WriteString(`<img height="1" width="1" src='`) 1326 dspImpTrackings2StrImg.WriteString(dspImpTrackings[i]) 1327 dspImpTrackings2StrImg.WriteString(`' > `) 1328 } 1329 1330 adm = "<style> html, body " + 1331 "{ margin: 0; padding: 0; width: 100%; height: 100%; vertical-align: middle; } " + 1332 "html " + 1333 "{ display: table; } " + 1334 "body { display: table-cell; vertical-align: middle; text-align: center; -webkit-text-size-adjust: none; } " + 1335 "</style> " + 1336 `<span class="title-link advertiser_label">` + imageTitle + "</span> " + 1337 "<a href='" + clickUrl + `' style="text-decoration:none" ` + 1338 "onclick=sendGetReq()> " + 1339 "<img src='" + imageInfoUrl + "' width='" + strconv.Itoa(int(adWidth)) + "' height='" + strconv.Itoa(int(adHeight)) + "'/> " + 1340 "</a> " + 1341 dspImpTrackings2StrImg.String() + 1342 `<script type="text/javascript">` + 1343 "var dspClickTrackings = [" + dspClickTrackings + "];" + 1344 "function sendGetReq() {" + 1345 "sendSomeGetReq(dspClickTrackings)" + 1346 "}" + 1347 "function sendOneGetReq(url) {" + 1348 "var req = new XMLHttpRequest();" + 1349 "req.open('GET', url, true);" + 1350 "req.send(null);" + 1351 "}" + 1352 "function sendSomeGetReq(urls) {" + 1353 "for (var i = 0; i < urls.length; i++) {" + 1354 "sendOneGetReq(urls[i]);" + 1355 "}" + 1356 "}" + 1357 "</script>" 1358 return adm, adWidth, adHeight, nil 1359 } 1360 1361 // for Interactiontype == appPromotion, clickUrl is intent 1362 func (a *adapter) getClickUrl(content *content) (clickUrl string, err error) { 1363 if content.Interactiontype == appPromotion { 1364 if content.MetaData.Intent != "" { 1365 clickUrl = getDecodeValue(content.MetaData.Intent) 1366 } else { 1367 return "", errors.New("content.MetaData.Intent in huaweiads resopnse is empty when interactiontype is appPromotion") 1368 } 1369 } else { 1370 if content.MetaData.ClickUrl != "" { 1371 clickUrl = content.MetaData.ClickUrl 1372 } else if content.MetaData.Intent != "" { 1373 clickUrl = getDecodeValue(content.MetaData.Intent) 1374 } 1375 } 1376 return clickUrl, nil 1377 } 1378 1379 func getDspImpClickTrackings(content *content) (dspImpTrackings []string, dspClickTrackings string) { 1380 for _, monitor := range content.Monitor { 1381 if len(monitor.Url) != 0 { 1382 switch monitor.EventType { 1383 case "imp": 1384 dspImpTrackings = monitor.Url 1385 case "click": 1386 dspClickTrackings = getStrings(monitor.Url) 1387 } 1388 } 1389 } 1390 return dspImpTrackings, dspClickTrackings 1391 } 1392 1393 func getStrings(eles []string) string { 1394 if len(eles) == 0 { 1395 return "" 1396 } 1397 var strs strings.Builder 1398 for i := 0; i < len(eles); i++ { 1399 strs.WriteString("\"" + eles[i] + "\"") 1400 if i < len(eles)-1 { 1401 strs.WriteString(",") 1402 } 1403 } 1404 return strs.String() 1405 } 1406 1407 // getDuration: millisecond -> format: 00:00:00.000 1408 func getDuration(duration int64) string { 1409 var dur time.Duration = time.Duration(duration) * time.Millisecond 1410 t := time.Time{}.Add(dur) 1411 return t.Format("15:04:05.000") 1412 } 1413 1414 // extractAdmVideo: get field adm for video, vast 3.0 1415 func (a *adapter) extractAdmVideo(adType int32, content *content, bidType openrtb_ext.BidType, opentrb2Imp openrtb2.Imp) (adm string, 1416 adWidth int64, adHeight int64, err error) { 1417 if content == nil { 1418 return "", 0, 0, errors.New("extract Adm for video failed: content is empty") 1419 } 1420 1421 var clickUrl = "" 1422 clickUrl, err = a.getClickUrl(content) 1423 if err != nil { 1424 return "", 0, 0, err 1425 } 1426 1427 var mime = "video/mp4" 1428 var resourceUrl = "" 1429 var duration = "" 1430 if adType == roll { 1431 // roll ad get information from mediafile 1432 if content.MetaData.MediaFile.Mime != "" { 1433 mime = content.MetaData.MediaFile.Mime 1434 } 1435 adWidth = content.MetaData.MediaFile.Width 1436 adHeight = content.MetaData.MediaFile.Height 1437 if content.MetaData.MediaFile.Url != "" { 1438 resourceUrl = content.MetaData.MediaFile.Url 1439 } else { 1440 return "", 0, 0, errors.New("extract Adm for video failed: Content.MetaData.MediaFile.Url is empty") 1441 } 1442 duration = getDuration(content.MetaData.Duration) 1443 } else { 1444 if content.MetaData.VideoInfo.VideoDownloadUrl != "" { 1445 resourceUrl = content.MetaData.VideoInfo.VideoDownloadUrl 1446 } else { 1447 return "", 0, 0, errors.New("extract Adm for video failed: content.MetaData.VideoInfo.VideoDownloadUrl is empty") 1448 } 1449 if content.MetaData.VideoInfo.Width != 0 && content.MetaData.VideoInfo.Height != 0 { 1450 adWidth = int64(content.MetaData.VideoInfo.Width) 1451 adHeight = int64(content.MetaData.VideoInfo.Height) 1452 } else if bidType == openrtb_ext.BidTypeVideo { 1453 if opentrb2Imp.Video != nil && opentrb2Imp.Video.W != nil && *opentrb2Imp.Video.W != 0 && opentrb2Imp.Video.H != nil && *opentrb2Imp.Video.H != 0 { 1454 adWidth = *opentrb2Imp.Video.W 1455 adHeight = *opentrb2Imp.Video.H 1456 } 1457 } else { 1458 return "", 0, 0, errors.New("extract Adm for video failed: cannot get video width, height") 1459 } 1460 duration = getDuration(int64(content.MetaData.VideoInfo.VideoDuration)) 1461 } 1462 1463 var adTitle = getDecodeValue(content.MetaData.Title) 1464 var adId = content.Contentid 1465 var creativeId = content.Contentid 1466 var trackingEvents strings.Builder 1467 var dspImpTracking2Str = "" 1468 var dspClickTracking2Str = "" 1469 var errorTracking2Str = "" 1470 for _, monitor := range content.Monitor { 1471 if len(monitor.Url) == 0 { 1472 continue 1473 } 1474 var event = "" 1475 switch monitor.EventType { 1476 case "vastError": 1477 errorTracking2Str = getVastImpClickErrorTrackingUrls(monitor.Url, "vastError") 1478 case "imp": 1479 dspImpTracking2Str = getVastImpClickErrorTrackingUrls(monitor.Url, "imp") 1480 case "click": 1481 dspClickTracking2Str = getVastImpClickErrorTrackingUrls(monitor.Url, "click") 1482 case "userclose": 1483 event = "skip&closeLinear" 1484 case "playStart": 1485 event = "start" 1486 case "playEnd": 1487 event = "complete" 1488 case "playResume": 1489 event = "resume" 1490 case "playPause": 1491 event = "pause" 1492 case "soundClickOff": 1493 event = "mute" 1494 case "soundClickOn": 1495 event = "unmute" 1496 default: 1497 } 1498 if event != "" { 1499 if event != "skip&closeLinear" { 1500 trackingEvents.WriteString(getVastEventTrackingUrls(monitor.Url, event)) 1501 } else { 1502 trackingEvents.WriteString(getVastEventTrackingUrls(monitor.Url, "skip&closeLinear")) 1503 } 1504 } 1505 } 1506 1507 // Only for rewarded video 1508 var rewardedVideoPart = "" 1509 var isAddRewardedVideoPart = true 1510 if adType == rewarded { 1511 var staticImageUrl = "" 1512 var staticImageHeight = "" 1513 var staticImageWidth = "" 1514 var staticImageType = "image/png" 1515 if len(content.MetaData.Icon) > 0 && content.MetaData.Icon[0].Url != "" { 1516 staticImageUrl = content.MetaData.Icon[0].Url 1517 if content.MetaData.Icon[0].Height > 0 && content.MetaData.Icon[0].Width > 0 { 1518 staticImageHeight = strconv.Itoa(int(content.MetaData.Icon[0].Height)) 1519 staticImageWidth = strconv.Itoa(int(content.MetaData.Icon[0].Width)) 1520 } else { 1521 staticImageHeight = strconv.Itoa(int(adHeight)) 1522 staticImageWidth = strconv.Itoa(int(adWidth)) 1523 } 1524 } else if len(content.MetaData.ImageInfo) > 0 && content.MetaData.ImageInfo[0].Url != "" { 1525 staticImageUrl = content.MetaData.ImageInfo[0].Url 1526 if content.MetaData.ImageInfo[0].Height > 0 && content.MetaData.ImageInfo[0].Width > 0 { 1527 staticImageHeight = strconv.Itoa(int(content.MetaData.ImageInfo[0].Height)) 1528 staticImageWidth = strconv.Itoa(int(content.MetaData.ImageInfo[0].Width)) 1529 } else { 1530 staticImageHeight = strconv.Itoa(int(adHeight)) 1531 staticImageWidth = strconv.Itoa(int(adWidth)) 1532 } 1533 } else { 1534 isAddRewardedVideoPart = false 1535 } 1536 if isAddRewardedVideoPart { 1537 rewardedVideoPart = `<Creative adId="` + adId + `" id="` + creativeId + `">` + 1538 "<CompanionAds>" + 1539 `<Companion width="` + staticImageWidth + `" height="` + staticImageHeight + `">` + 1540 `<StaticResource creativeType="` + staticImageType + `"><![CDATA[` + staticImageUrl + `]]></StaticResource>` + 1541 "<CompanionClickThrough><![CDATA[" + clickUrl + "]]></CompanionClickThrough>" + 1542 "</Companion>" + 1543 "</CompanionAds>" + 1544 "</Creative>" 1545 } 1546 } 1547 1548 adm = `<?xml version="1.0" encoding="UTF-8"?>` + 1549 `<VAST version="3.0">` + 1550 `<Ad id="` + adId + `"><InLine>` + 1551 "<AdSystem>HuaweiAds</AdSystem>" + 1552 "<AdTitle>" + adTitle + "</AdTitle>" + 1553 errorTracking2Str + dspImpTracking2Str + 1554 "<Creatives>" + 1555 `<Creative adId="` + adId + `" id="` + creativeId + `">` + 1556 "<Linear>" + 1557 "<Duration>" + duration + "</Duration>" + 1558 "<TrackingEvents>" + trackingEvents.String() + "</TrackingEvents>" + 1559 "<VideoClicks>" + 1560 "<ClickThrough><![CDATA[" + clickUrl + "]]></ClickThrough>" + 1561 dspClickTracking2Str + 1562 "</VideoClicks>" + 1563 "<MediaFiles>" + 1564 `<MediaFile delivery="progressive" type="` + mime + `" width="` + strconv.Itoa(int(adWidth)) + `" ` + 1565 `height="` + strconv.Itoa(int(adHeight)) + `" scalable="true" maintainAspectRatio="true"> ` + 1566 "<![CDATA[" + resourceUrl + "]]>" + 1567 "</MediaFile>" + 1568 "</MediaFiles>" + 1569 "</Linear>" + 1570 "</Creative>" + rewardedVideoPart + 1571 "</Creatives>" + 1572 "</InLine>" + 1573 "</Ad>" + 1574 "</VAST>" 1575 return adm, adWidth, adHeight, nil 1576 } 1577 1578 func getVastImpClickErrorTrackingUrls(urls []string, eventType string) (result string) { 1579 var trackingUrls strings.Builder 1580 for _, url := range urls { 1581 if eventType == "click" { 1582 trackingUrls.WriteString("<ClickTracking><![CDATA[") 1583 trackingUrls.WriteString(url) 1584 trackingUrls.WriteString("]]></ClickTracking>") 1585 } else if eventType == "imp" { 1586 trackingUrls.WriteString("<Impression><![CDATA[") 1587 trackingUrls.WriteString(url) 1588 trackingUrls.WriteString("]]></Impression>") 1589 } else if eventType == "vastError" { 1590 trackingUrls.WriteString("<Error><![CDATA[") 1591 trackingUrls.WriteString(url) 1592 trackingUrls.WriteString("&et=[ERRORCODE]]]></Error>") 1593 } 1594 } 1595 return trackingUrls.String() 1596 } 1597 1598 func getVastEventTrackingUrls(urls []string, eventType string) (result string) { 1599 var trackingUrls strings.Builder 1600 for _, eventUrl := range urls { 1601 if eventType == "skip&closeLinear" { 1602 trackingUrls.WriteString(`<Tracking event="skip"><![CDATA[`) 1603 trackingUrls.WriteString(eventUrl) 1604 trackingUrls.WriteString(`]]></Tracking><Tracking event="closeLinear"><![CDATA[`) 1605 trackingUrls.WriteString(eventUrl) 1606 trackingUrls.WriteString("]]></Tracking>") 1607 } else { 1608 trackingUrls.WriteString(`<Tracking event="`) 1609 trackingUrls.WriteString(eventType) 1610 trackingUrls.WriteString(`"><![CDATA[`) 1611 trackingUrls.WriteString(eventUrl) 1612 trackingUrls.WriteString("]]></Tracking>") 1613 } 1614 } 1615 return trackingUrls.String() 1616 } 1617 1618 func computeHmacSha256(message string, signKey string) string { 1619 h := hmac.New(sha256.New, []byte(signKey)) 1620 h.Write([]byte(message)) 1621 return hex.EncodeToString(h.Sum(nil)) 1622 } 1623 1624 // getDigestAuthorization: get digest authorization for request header 1625 func getDigestAuthorization(huaweiAdsImpExt *openrtb_ext.ExtImpHuaweiAds, isTestAuthorization bool) string { 1626 var nonce = strconv.FormatInt(time.Now().UnixNano()/1e6, 10) 1627 // this is for test case, time 2021/8/20 19:30 1628 if isTestAuthorization { 1629 nonce = "1629473330823" 1630 } 1631 publisher_id := strings.TrimSpace(huaweiAdsImpExt.PublisherId) 1632 sign_key := strings.TrimSpace(huaweiAdsImpExt.SignKey) 1633 key_id := strings.TrimSpace(huaweiAdsImpExt.KeyId) 1634 1635 var apiKey = publisher_id + ":ppsadx/getResult:" + sign_key 1636 return "Digest username=" + publisher_id + "," + 1637 "realm=ppsadx/getResult," + 1638 "nonce=" + nonce + "," + 1639 "response=" + computeHmacSha256(nonce+":POST:/ppsadx/getResult", apiKey) + "," + 1640 "algorithm=HmacSHA256,usertype=1,keyid=" + key_id 1641 }