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