Skip to content

Commit

Permalink
feat: add get bulk upload url api support
Browse files Browse the repository at this point in the history
  • Loading branch information
koladilip committed Apr 19, 2023
1 parent fb9e219 commit 8a23e71
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test
coverage.txt

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

token.json

.env

29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
install:
sh install-hooks.sh
default: build

test: install-tools test-run

test-run:
gotestsum --format pkgname-and-test-fails -- -count=1 -shuffle=on -coverprofile=coverage.txt -vet=all ./...

build:
go build -o bin/$(NAME) ./cmd/$(NAME).go

test-with-coverage: test coverage

coverage:
go tool cover -html=coverage.txt -o coverage.html

install-tools:
go install mvdan.cc/gofumpt@latest
go install gotest.tools/gotestsum@v1.8.2

.PHONY: lint
lint: fmt ## Run linters on all go files
docker run --rm -v $(shell pwd):/app:ro -w /app golangci/golangci-lint:v1.51.1 bash -e -c \
'golangci-lint run -v --timeout 5m'

.PHONY: fmt
fmt: install-tools ## Formats all go files
gofumpt -l -w -extra .
69 changes: 69 additions & 0 deletions bingads/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package bingads

import (
"encoding/xml"
"net/http"

"golang.org/x/oauth2"
)

var (
EnvelopeNamespace = "http://schemas.xmlsoap.org/soap/envelope/"
BingNamespace = "https://bingads.microsoft.com/CampaignManagement/v13"
AuthEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
)

type RequestEnvelope struct {
XMLName xml.Name `xml:"s:Envelope"`
EnvNS string `xml:"xmlns:i,attr"`
EnvSS string `xml:"xmlns:s,attr"`
Header RequestHeader
Body RequestBody
}

type SoapResponseBody struct {
OperationResponse []byte `xml:",innerxml"`
}

type SoapResponseEnvelope struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
Header TrackingId `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"`
Body SoapResponseBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
}

type TrackingId struct {
Nil bool `xml:"http://www.w3.org/2001/XMLSchema-instance nil,attr"`
TrackingId string `xml:"https://adcenter.microsoft.com/v8 TrackingId"`
}

type RequestBody struct {
XMLName xml.Name `xml:"s:Body"`
Body interface{}
}

type RequestHeader struct {
XMLName xml.Name `xml:"s:Header"`
BingNS string `xml:"xmlns,attr"`
Action string
AuthenticationToken string `xml:"AuthenticationToken,omitempty"`
CustomerAccountId string `xml:"CustomerAccountId"`
CustomerId string `xml:"CustomerId,omitempty"`
DeveloperToken string `xml:"DeveloperToken"`
Password string `xml:"Password,omitempty"`
Username string `xml:"UserName,omitempty"`
}

type Session struct {
AccountId string
CustomerId string
DeveloperToken string
Username string
Password string
HTTPClient HttpClient
TokenSource oauth2.TokenSource
}

type HttpClient interface {
Do(*http.Request) (*http.Response, error)
}
51 changes: 51 additions & 0 deletions bingads/bulk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package bingads

import (
"encoding/xml"
"strconv"
)

type BulkService struct {
Endpoint string
Session *Session
}

func NewBulkService(session *Session) *BulkService {
return &BulkService{
Endpoint: "https://bulk.api.bingads.microsoft.com/Api/Advertiser/CampaignManagement/v13/BulkService.svc",
Session: session,
}
}

type GetBulkUploadUrlRequest struct {
XMLName xml.Name `xml:"GetBulkUploadUrlRequest"`
NS string `xml:"xmlns,attr"`
AccountId int64 `xml:"AccountId"`
ResponseMode string `xml:"ResponseMode"`
}

type GetBulkUploadUrlResponse struct {
UploadUrl string `xml:"UploadUrl"`
RequestId string `xml:"RequestId"`
}

// GetBulkCampaignsByAccountId
func (c *BulkService) GetBulkUploadUrl() (*GetBulkUploadUrlResponse, error) {
accountId, _ := strconv.ParseInt(c.Session.AccountId, 10, 64)
req := GetBulkUploadUrlRequest{
NS: BingNamespace,
AccountId: accountId,
ResponseMode: "ErrorsOnly",
}

resp, err := c.Session.SendRequest(req, c.Endpoint, "GetBulkUploadUrl")
if err != nil {
return nil, err
}

var getBulkUploadUrlResponse GetBulkUploadUrlResponse
if err = xml.Unmarshal(resp, &getBulkUploadUrlResponse); err != nil {
return nil, err
}
return &getBulkUploadUrlResponse, nil
}
134 changes: 134 additions & 0 deletions bingads/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package bingads

import (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"os"

"golang.org/x/oauth2"
)

var debug = os.Getenv("BING_SDK_DEBUG")

func (b *Session) SendRequest(body interface{}, endpoint, soapAction string) ([]byte, error) {
return b.sendRequest(body, endpoint, soapAction, campaignns)
}

var campaignns = "https://bingads.microsoft.com/CampaignManagement/v13"

// FIXME: namespace
func (b *Session) sendRequest(body interface{}, endpoint, soapAction, ns string) ([]byte, error) {
header := RequestHeader{
BingNS: ns,
Action: soapAction,
CustomerAccountId: b.AccountId,
CustomerId: b.CustomerId,
DeveloperToken: b.DeveloperToken,
}
if b.TokenSource != nil {
token, err := b.TokenSource.Token()
if err != nil {
return nil, err
}
header.AuthenticationToken = token.AccessToken
} else {
header.Username = b.Username
header.Password = b.Password
}

envelope := RequestEnvelope{
EnvNS: "http://www.w3.org/2001/XMLSchema-instance",
EnvSS: "http://schemas.xmlsoap.org/soap/envelope/",
Header: header,
Body: RequestBody{
Body: body,
},
}

req, err := xml.MarshalIndent(envelope, "", " ")
if err != nil {
return nil, err
}

httpRequest, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(req))
if err != nil {
return nil, err
}

httpRequest.Header.Add("Content-Type", "text/xml; charset=utf-8")
httpRequest.Header.Add("SOAPAction", soapAction)

response, err := b.HTTPClient.Do(httpRequest)
if err != nil {
return nil, err
}

raw, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}

if debug != "" {
fmt.Println(string(req))
}
// fmt.Println(string(raw))

res := SoapResponseEnvelope{}

err = xml.Unmarshal(raw, &res)
if err != nil {
return nil, err
}

if debug != "" {
fmt.Println(">>>")
fmt.Println(string(res.Body.OperationResponse))
fmt.Println(">>>")
}

switch response.StatusCode {
case 400, 401, 403, 405, 500:
fault := Fault{}
err = xml.Unmarshal(res.Body.OperationResponse, &fault)
if err != nil {
return res.Body.OperationResponse, err
}
for _, e := range fault.Detail.Errors.AdApiErrors {
switch e.ErrorCode {
case "AuthenticationTokenExpired", "InvalidCredentials", "InternalError", "CallRateExceeded":
return res.Body.OperationResponse, &baseError{
code: e.ErrorCode,
origErr: &fault.Detail.Errors,
}
}
}
return res.Body.OperationResponse, &fault.Detail.Errors
}

return res.Body.OperationResponse, err
}

type SessionConfig struct {
OAuth2Config *oauth2.Config
OAuth2Token *oauth2.Token
AccountId string
CustomerId string
DeveloperToken string
HTTPClient HttpClient
}

func NewSession(config SessionConfig) *Session {
tokenSource := config.OAuth2Config.TokenSource(context.TODO(), config.OAuth2Token)

return &Session{
AccountId: config.AccountId,
CustomerId: config.CustomerId,
DeveloperToken: config.DeveloperToken,
HTTPClient: config.HTTPClient,
TokenSource: tokenSource,
}
}
68 changes: 68 additions & 0 deletions bingads/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package bingads

import (
"encoding/xml"
"strings"
)

type baseError struct {
code string
origErr error
}

func (b baseError) Error() string {
return b.origErr.Error()
}

func (b baseError) String() string {
return b.Error()
}

func (b baseError) Code() string {
return b.code
}

func (b baseError) OrigErr() error {
return b.origErr
}

type AdApiError struct {
Code int64 `xml:"AdApiError>Code"`
Details string `xml:"AdApiError>Details"`
ErrorCode string `xml:"AdApiError>ErrorCode"`
Message string `xml:"AdApiError>Message"`
}

type OperationError struct {
Code int64 `xml:"OperationError>Code"`
Details string `xml:"OperationError>Details"`
ErrorCode string `xml:"OperationError>ErrorCode"`
Message string `xml:"OperationError>Message"`
}

type Fault struct {
FaultCode string `xml:"faultcode"`
FaultString string `xml:"faultstring"`
Detail struct {
XMLName xml.Name `xml:"detail"`
Errors ErrorsType `xml:",any"`
}
}

type ErrorsType struct {
TrackingId string `xml:"TrackingId"`
AdApiErrors []AdApiError `xml:"Errors"`
OperationErrors []OperationError `xml:"OperationErrors"`
}

func (f *ErrorsType) Error() string {
errors := []string{}
for _, e := range f.AdApiErrors {
errors = append(errors, e.Message)
}

for _, e := range f.OperationErrors {
errors = append(errors, e.Message)
}
return strings.Join(errors, "\n")
}
3 changes: 3 additions & 0 deletions githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
make fmt
make test
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/rudderlabs/bing-ads-go-sdk

go 1.20

require golang.org/x/oauth2 v0.7.0

require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/joho/godotenv v1.5.1
golang.org/x/net v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
Loading

0 comments on commit 8a23e71

Please sign in to comment.