├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── createCampaign.js ├── createOrders.js ├── customTargetingService.js ├── dfpCredentials.js ├── listLineitems.js └── runReport.js ├── gruntfile.js ├── index.js ├── lib ├── Dfp.js ├── DfpUser.js └── DfpUtils.js ├── package.json └── spec ├── DfpUser-spec.js ├── DfpUtils-spec.js └── FF4D00-0.8.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *sublime-* 3 | local-test 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser" : true, 3 | "node" : true, 4 | "esnext" : true, 5 | "globals" : {}, 6 | "globalstrict" : false, 7 | "laxbreak" : true, 8 | "multistr" : true, 9 | "smarttabs" : false, 10 | "trailing" : true, 11 | "undef" : false, 12 | "unused" : true, 13 | "indent" : 2 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | before_install: 5 | - npm install -g grunt-cli 6 | - npm install -g jasmine-node 7 | - npm install -g istanbul 8 | - npm install -g codeclimate-test-reporter 9 | sudo: false 10 | script: 11 | - grunt lint 12 | - jasmine-node . 13 | addons: 14 | code_climate: 15 | repo_token:a9ce2000c9aa1298fe539c2e6479d56e60d210336c6f8f189a5b4a4b6d774a2e 16 | after_script: 17 | - istanbul cover jasmine-node . 18 | - CODECLIMATE_REPO_TOKEN=a9ce2000c9aa1298fe539c2e6479d56e60d210336c6f8f189a5b4a4b6d774a2e codeclimate-test-reporter < coverage/lcov.info -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Shiny Ads 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://codeclimate.com/github/ShinyAds/node-google-dfp/badges/gpa.svg)](https://codeclimate.com/github/ShinyAds/node-google-dfp) [![Test Coverage](https://codeclimate.com/github/ShinyAds/node-google-dfp/badges/coverage.svg)](https://codeclimate.com/github/ShinyAds/node-google-dfp/coverage) [![Build Status](https://travis-ci.org/ShinyAds/node-google-dfp.svg?branch=master)](https://travis-ci.org/ShinyAds/node-google-dfp) 2 | # Google DFP API Client for NodeJS 3 | 4 | ## Basics 5 | 6 | Initialize the DFP Instance. 7 | 8 | ```JavaScript 9 | var Dfp = require('node-google-dfp'), 10 | dfpUser = new Dfp.User(NETWORK_CODE, APP_NAME, VERSION); 11 | ``` 12 | 13 | Next, setup your client settings and your user's OAUTH token information. 14 | 15 | ```JavaScript 16 | dfpUser.setSettings({ 17 | client_id : "YOUR CLIENT ID", 18 | client_secret : "YOUR CLIENT SECRET", 19 | refresh_token : "A REFRESH TOKEN", 20 | redirect_url : "YOUR OAUTH REDIRECT URL" 21 | }); 22 | ``` 23 | 24 | You can instance any of DFP's API Services; https://developers.google.com/doubleclick-publishers/docs/start 25 | 26 | 27 | ```JavaScript 28 | dfpUser.getService('LineItemService', function (err, lineItemService) { 29 | if (err) { 30 | return console.error(err); 31 | } 32 | 33 | var statement = new DfpClass.Statement('WHERE id = 103207340'); 34 | 35 | lineItemService.getLineItemsByStatement(statement, function (err, results) { 36 | console.log(results); 37 | }); 38 | 39 | }); 40 | ``` 41 | 42 | ### Service accounts example 43 | 44 | If you would like to use a Google [Service Account](https://developers.google.com/doubleclick-publishers/docs/service_accounts) to access DFP, you can do so by creating an instance of the JWT auth client. 45 | 46 | ```JavaScript 47 | var {JWT} = require('google-auth-library') 48 | 49 | var jwtClient = new JWT( 50 | SERVICE_ACCOUNT_EMAIL, 51 | 'path/to/key.pem', 52 | null, 53 | ['https://www.googleapis.com/auth/dfp']); 54 | 55 | dfpUser.setClient(jwtClient) 56 | 57 | ``` 58 | 59 | ======= 60 | ### oAuth setup 61 | 62 | This application requires a working oAuth refresh token to make requests. If you don't include a refresh token you will get an "No refresh token is set" error. If you include a bad token, you'll get an "illegal access" error. Service accounts are not supported. 63 | 64 | To setup a refresh token manually, follow [Google's instructions](https://developers.google.com/accounts/docs/OAuth2ForDevices#obtainingatoken) for using cURL. The main steps are included below. 65 | 66 | * Setup a oAuth "installed application" in the Google Developer Console. 67 | 68 | * Create a verification request using this installed application's client ID. (If you miss this step you'll get an `authorization_pending` error from Google on the next step.) Note that any slashes in a `device_code` will need to be escaped. 69 | 70 | ```curl -d "client_id={YOUR_OAUTH_CLIENT_ID}&scope=https://www.googleapis.com/auth/dfp" https://accounts.google.com/o/oauth2/device/code``` 71 | 72 | { 73 | "device_code" : "ABCD-EFGH4/MEiMYvOO1THXLV_fHGGN8obAgb5XFs1Uctj-QsyYsQk", 74 | "user_code" : "ABCD-EFGH", 75 | "verification_url" : "https://www.google.com/device", 76 | "expires_in" : 1800, 77 | "interval" : 5 78 | } 79 | 80 | 81 | * Visit the `verification_url` contained in the verification request response (e.g. https://www.google.com/device) in a browser and type in the `user_code` provided in the verification response. A verification code will be given as a response. 82 | 83 | * Use the provided `code` and your client ID and secret from the Google oAuth application to create a refresh token. 84 | 85 | ```curl -d "client_id={YOUR_OAUTH_CLIENT_ID}&client_secret={YOUR_OAUTH_CLIENT_SECRET}&code={YOUR_VERIFICATION_CODE}&grant_type=http://oauth.net/grant_type/device/1.0" https://accounts.google.com/o/oauth2/token``` 86 | 87 | { 88 | "access_token" : "ya29.JAHynQpVpjBFhvg-7VKdQ7nmD0DkmCYoWTWo535TP8QsKa6j2rFOI1i0pdclFepv_GZo9A2SrN41dA", 89 | "token_type" : "Bearer", 90 | "expires_in" : 3600, 91 | "id_token" : "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdjZmM3YTJhMDkwYWJjOGYxMDU5MjJmMzFiN2FjZGUzYzA2NmU1NTYifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiaWQiOiIxMTU2MjQxNzA0MjQ3NzA5NDYzNzgiLCJzdWIiOiIxMTU2MjQxNzA0MjQ3NzA5NDYzNzgiLCJhenAiOiI4MzQ3MDQ2OTI1ODItMzlwY3I2M2RmNjBlZjByY2E5ZTc1cDRicTlzbjhxOWUuYXBwcy5nb29nbyV1c2VyY29udGVudC5jb20iLCJlbWFpbCI6InRheWxvci5idWxleUBtY25hdWdodG9uLm1lZGlhIiwiYXRfaGFzaCI6Ikp2Sl9JUDlxUk9zX1JUNDBoY0FSWVEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiODM0NzA0HjkyNTgyLTM5cGNyNjNkZjYwZWYwcmNhOWU3NXA0YnE5c244cTllLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJtY25hdWdodG9uLm1lZGlhIiwidG9rZW5faGFzaCI6Ikp2Sl9JSDlxUk9zX1JUNDBoY0FSWVEiLCJ2ZXJpZmllZF9lbWFpbCI6dHJ1ZSwiY2lkIjoiODM0NzA0NjkyNTgyLTM5cGNyNjNkZjYwZWYwcmNhOWU3NXA0YnE5c244cTllLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaWF0IjoxNDI0ODAwNDcwLCJleHAiOjE0MjQ4MDQzNzB9.T9mTcSl5bJLKrFhldXOd1L1CnGFfZHNF1eQOmJYyp7wR3vKbz8ATTNAfyo8_2hSGt9kGrHDBcdgaq_18RYS72Tt0MclNy020romjl6rYRjs6GH93S3ZMiwra3UI3kmDXym9kyntedMS5gIPgJWfcoh0J0CTDNPBisLNrZntJv7Y", 92 | "refresh_token" : "1/CGpCHgTgJ28PMnh84PgQBOgHHHaLCDbDQ_0ZiINmO_g" 93 | } 94 | 95 | You can use `urn:ietf:wg:oauth:2.0:oob` for the redirect URL of non-public apps. 96 | 97 | Known Issues 98 | ------------ 99 | 100 | 1. OAuth Support for more than just Refresh Token 101 | 2. No unit tests 102 | 103 | 104 | How to contribute 105 | ----------- 106 | 107 | Follow Github's [recommended workflow](https://help.github.com/articles/fork-a-repo) for contributing to this project. 108 | 109 | 1. Fork it 110 | 2. Create your feature branch (`git checkout -b your-new-feature`) 111 | 3. Commit your changes (`git commit -am 'Add some feature'`) 112 | 4. Push to the branch (`git push origin your-new-feature`) 113 | 5. Create new Pull Request 114 | -------------------------------------------------------------------------------- /examples/createCampaign.js: -------------------------------------------------------------------------------- 1 | // List lineitems 2 | 3 | var Dfp = require('node-google-dfp'); 4 | var dfpConfig = require('./dfpCredentials'); 5 | 6 | var dfpUser = new Dfp.User(dfpConfig.networkCode, dfpConfig.applicationName); 7 | dfpUser.setSettings(dfpConfig); 8 | 9 | dfpUser.getService('OrderService', function (err, orderService) { 10 | if (err) { 11 | console.error(err); 12 | return false; 13 | } 14 | 15 | var args = { orders: [{ 16 | name: 'Full Campaign #67', 17 | notes: 'It cannot be this simple!', 18 | advertiserId: '10209990', // Must correspond to an advertiserId in your DFP instance 19 | traffickerId: '50819180' // Must correspond to an traffickerId in your DFP instance 20 | }]}; 21 | 22 | orderService.createOrders(args, function (err, myOrder) { 23 | if (err) { 24 | console.log(err.response.body); 25 | return false; 26 | } 27 | console.log(myOrder); 28 | 29 | dfpUser.getService('LineItemService', function (err, lineItemService) { 30 | if (err) { 31 | console.error(err); 32 | return false; 33 | } 34 | 35 | var li = { lineItems: [{ 36 | orderId: parseFloat(myOrder.rval[0].id), 37 | name: 'Lineitem descriptive name #10', 38 | externalId: '#122412', 39 | startDateTime: Dfp.DfpDate.from(new Date(), 'America/Toronto'), 40 | startDateTimeType: 'IMMEDIATELY', 41 | endDateTime: Dfp.DfpDate.from(new Date(), 'America/Toronto', 0, 1), 42 | creativeRotationType: 'EVEN', 43 | lineItemType: 'STANDARD', 44 | priority: 8, 45 | unitsBought: 100000, 46 | costPerUnit: Dfp.Money(5.6, 'CAD'), 47 | costType: 'CPM', 48 | creativePlaceholders: { size: { 49 | width: 728, 50 | height: 90, 51 | isAspectRatio: false 52 | }}, 53 | targetPlatform: 'ANY', 54 | targeting: { inventoryTargeting: { targetedPlacementIds: '3980'} } 55 | }]}; 56 | 57 | lineItemService.createLineItems(li, function (err, myLineItem) { 58 | if (err) { 59 | console.log(err.response.body); 60 | return false; 61 | } 62 | console.log(myLineItem); 63 | 64 | dfpUser.getService('CreativeService', function (err, creativeService) { 65 | if (err) { 66 | console.error(err); 67 | return false; 68 | } 69 | 70 | var cr = { creatives: [{ 71 | attributes: { 'xsi:type': 'ImageCreative' }, // Read Creative.Type - https://developers.google.com/doubleclick-publishers/docs/reference/v201403/CreativeService.BaseImageCreative 72 | advertiserId: '10209990', // Must correspond to an advertiserId in your DFP instance 73 | name: 'My Creative #1', 74 | size: { 75 | width: 728, 76 | height: 90, 77 | isAspectRatio: false 78 | }, 79 | destinationUrl: 'www.google.com', 80 | primaryImageAsset: { 81 | assetByteArray: Dfp.assetByteArray('/home/img/Pictures/myPic.jpg'), // Use your own creative 82 | fileName: 'image_name_001' 83 | } 84 | }]}; 85 | 86 | creativeService.createCreatives(cr, function (err, myCreative) { 87 | if (err) { 88 | console.log(err.response.body); 89 | return false; 90 | } 91 | console.log(myCreative); 92 | 93 | dfpUser.getService('LineItemCreativeAssociationService', function (err, licaService) { 94 | if (err) { 95 | console.error(err); 96 | return false; 97 | } 98 | 99 | var lica = { lineItemCreativeAssociations: [{ 100 | lineItemId: myLineItem.rval[0].id, 101 | creativeId: myCreative.rval[0].id 102 | }]}; 103 | 104 | licaService.createLineItemCreativeAssociations(lica, function (err, myLICA) { 105 | if (err) { 106 | console.log(err.response.body); 107 | return false; 108 | } 109 | console.log(myLICA); 110 | 111 | var approveAndOverbook = { 112 | orderAction: { 113 | attributes: { 'xsi:type': 'ApproveAndOverbookOrders' }, 114 | skipInventoryCheck: true 115 | }, 116 | filterStatement: { query: 'WHERE id = ' + myOrder.rval[0].id } 117 | }; 118 | 119 | orderService.performOrderAction(approveAndOverbook, function (err, approval) { 120 | if (err) { 121 | console.log(err.response.body); 122 | return false; 123 | } 124 | console.log(approval); 125 | }); 126 | }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /examples/createOrders.js: -------------------------------------------------------------------------------- 1 | // List lineitems 2 | 3 | var Dfp = require('../lib/Dfp'); 4 | var dfpConfig = require('/dfpCredentials'); 5 | 6 | var dfpUser = new Dfp.User(dfpConfig.networkCode, dfpConfig.applicationName); 7 | dfpUser.setSettings(dfpConfig); 8 | 9 | dfpUser.getService('OrderService', function (err, orderService) { 10 | if (err) { 11 | return console.error(err); 12 | } 13 | 14 | var args = { 15 | orders: [ 16 | { 17 | name: 'Multiple NodeJS Order Creation #1', 18 | notes: 'It cannot be this simple!', 19 | advertiserId: '10209990', // Must correspond to an advertiserId in your DFP instance 20 | traffickerId: '50819180' // Must correspond to an traffickerId in your DFP instance 21 | }, 22 | { 23 | name: 'Multiple NodeJS Order Creation #2', 24 | notes: 'Really???', 25 | advertiserId: '10209990', 26 | traffickerId: '50819180' 27 | } 28 | ] 29 | }; 30 | 31 | orderService.createOrders(args, function (err, results) { 32 | if (err) { 33 | console.log(err.response.body); 34 | return false; 35 | } 36 | console.log(results); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /examples/customTargetingService.js: -------------------------------------------------------------------------------- 1 | var Dfp = require('node-google-dfp'); 2 | var dfpConfig = require('./dfpCredentials'); 3 | 4 | var dfpUser = new Dfp.User(dfpConfig.networkCode, dfpConfig.applicationName); 5 | dfpUser.setSettings(dfpConfig); 6 | 7 | 8 | //Documentation : https://developers.google.com/ad-manager/api/reference/v201802/CustomTargetingService 9 | 10 | 11 | dfpUser.getService('CustomTargetingService', (err, targetingService) => { 12 | if (err) throw err; 13 | 14 | //get customTargetingKeys 15 | getCustomTargetingKeys(targetingService, (err, result) => { 16 | if (err) throw err; 17 | console.log(result); 18 | //expected result: 19 | 20 | // { 21 | // "rval": { 22 | // "totalResultSetSize": 1, 23 | // "startIndex": 0, 24 | // "results": [ 25 | // { 26 | // "id": "123456789", 27 | // "name": "AB", 28 | // "displayName": "ABE", 29 | // "type": "FREEFORM", 30 | // "status": "ACTIVE" 31 | // } 32 | // ] 33 | // } 34 | // } 35 | }); 36 | 37 | 38 | //update customTargetingKeys 39 | updateCustomTargetingKeys(targetingService, (err, result) => { 40 | if (err) throw err; 41 | console.log(result); 42 | //expected result: 43 | 44 | // { 45 | // "rval": [ 46 | // { 47 | // "id": "123456789", 48 | // "name": "ABCD", 49 | // "displayName": "ABCDE", 50 | // "type": "FREEFORM", 51 | // "status": "ACTIVE" 52 | // } 53 | // ] 54 | // } 55 | }); 56 | 57 | 58 | //get customTargetingValues 59 | getCustomTargetingValues(targetingService, (err, result) => { 60 | if (err) throw err; 61 | console.log(result); 62 | 63 | //expected result: 64 | 65 | // { 66 | // "rval": { 67 | // "totalResultSetSize": 1, 68 | // "startIndex": 0, 69 | // "results": [ 70 | // { 71 | // "customTargetingKeyId":"123456789", 72 | // "id": "1234567897384", 73 | // "name": "DCE", 74 | // "displayName": "DC", 75 | // "matchType":"EXACT", 76 | // "status": "ACTIVE" 77 | // } 78 | // ] 79 | // } 80 | // } 81 | 82 | }); 83 | 84 | //updateCustomTargetingValues 85 | updateCustomTargetingValues(targetingService, (err, result) => { 86 | if (err) throw err; 87 | console.log(result); 88 | //expected result: 89 | 90 | // { 91 | // "rval": [ 92 | // { 93 | // "customTargetingKeyId":"123456789", 94 | // "id": "1234567897384", 95 | // "name": "DCERZ", 96 | // "displayName": "DCERZT", 97 | // "matchType":"EXACT", 98 | // "status": "ACTIVE" 99 | // } 100 | // ] 101 | // } 102 | }); 103 | 104 | }); 105 | 106 | 107 | let getCustomTargetingKeys = (targetingService, done) => { 108 | let query = new Dfp.Statement("WHERE status='ACTIVE'");//put your query statement 109 | targetingService.getCustomTargetingKeysByStatement(query, (err, results) => { 110 | if (err) throw err; 111 | done(null, results); 112 | }); 113 | }; 114 | 115 | 116 | let getCustomTargetingValues = (targetingService, done) => { 117 | let query = new Dfp.Statement(`WHERE customTargetingKeyId='11704510'`); //put your query statement 118 | targetingService.getCustomTargetingValuesByStatement(query, (err, results) => { 119 | if (err) throw err; 120 | done(null, results); 121 | }); 122 | }; 123 | 124 | 125 | let updateCustomTargetingKeys = (targetingService, done) => { 126 | var args = { keys: [{ 127 | "id":"123456789", 128 | "name":"ABCD", 129 | "displayName":"ABCDE", 130 | "type":"FREEFORM", 131 | "status":"ACTIVE"} 132 | ]}; 133 | targetingService.updateCustomTargetingKeys(args, (err, results) => { 134 | if (err) throw err; 135 | done(null, results); 136 | }); 137 | }; 138 | 139 | 140 | let updateCustomTargetingValues = (targetingService, done) => { 141 | var args = { values: [{ 142 | "customTargetingKeyId":"11704510", 143 | "id":"1234567897384", 144 | "name":"DCERZ", 145 | "displayName":"DCERZT", 146 | "matchType":"EXACT", 147 | "status":"ACTIVE"} 148 | ]}; 149 | targetingService.updateCustomTargetingValues(args, (err, results) => { 150 | if (err) throw err; 151 | done(null, results); 152 | }); 153 | }; 154 | -------------------------------------------------------------------------------- /examples/dfpCredentials.js: -------------------------------------------------------------------------------- 1 | // DfpCredentials 2 | module.exports = { 3 | applicationName : "", // Your application's name 4 | client_id : "", // Your application's DFP ID 5 | redirect_url : "", // Your application's URL for OAUTH redirections 6 | 7 | networkCode : 0, // The publisher's DFP network code 8 | client_secret : "", // The publisher's OAUTH client secret 9 | refresh_token : "" // The publisher's OAUTH refresh token 10 | }; 11 | -------------------------------------------------------------------------------- /examples/listLineitems.js: -------------------------------------------------------------------------------- 1 | // List lineitems 2 | 3 | var Dfp = require('node-google-dfp'); 4 | var dfpConfig = require('./dfpCredentials'); 5 | 6 | var dfpUser = new Dfp.User(dfpConfig.networkCode, dfpConfig.applicationName); 7 | dfpUser.setSettings(dfpConfig); 8 | 9 | dfpUser.getService('LineItemService', function (err, lineItemService) { 10 | if (err) { 11 | return console.error(err); 12 | } 13 | 14 | var query = new Dfp.Statement('LIMIT 10'); 15 | 16 | lineItemService.getLineItemsByStatement(query, function (err, results) { 17 | if (err) 18 | return console.log('ERROR', err); 19 | console.log('results', results); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/runReport.js: -------------------------------------------------------------------------------- 1 | // run a report on line item #1234567 2 | 3 | var Dfp = require('node-google-dfp'); 4 | var dfpConfig = require('./dfpCredentials'); 5 | 6 | var dfpUser = new Dfp.User(dfpConfig.networkCode, dfpConfig.applicationName); 7 | dfpUser.setSettings(dfpConfig); 8 | 9 | dfpUser.getService('ReportService', function (err, reportService) { 10 | if (err) { 11 | return console.error(err); 12 | } 13 | 14 | var results = null; 15 | var args = { 16 | reportJob: { 17 | reportQuery: { 18 | dimensions: ['DATE'], 19 | columns: [ 'AD_SERVER_CLICKS', 'AD_SERVER_IMPRESSIONS' ], 20 | dimensionAttributes : [], 21 | startDate: { year: 2014, month: 5, day: 22 }, 22 | endDate: { year: 2014, month: 6, day: 21 }, 23 | dimensionFilters : [], 24 | statement: { query : 'WHERE LINE_ITEM_ID = 123456789'} 25 | } 26 | } 27 | }; 28 | 29 | 30 | function download_report(download_url, local_filename) { 31 | var https = require('https'); 32 | var fs = require('fs'); 33 | var zlib = require('zlib'); 34 | 35 | var file = fs.createWriteStream(local_filename); 36 | https.get(download_url, function (response) { 37 | response.pipe(zlib.createGunzip()).pipe(file); 38 | }); 39 | } 40 | 41 | 42 | function check_report_ready() { 43 | 44 | var reportId = results.rval.id; 45 | console.log('Trying to get report #' + reportId); 46 | 47 | reportService.getReportJobStatus({reportJobId : reportId}, function (err, data) { 48 | 49 | if (err) { 50 | return console.log('ERROR', err); 51 | } 52 | console.log('Report Job #' + reportId + ' returned ' + data.rval); 53 | 54 | if (data.rval === 'COMPLETED') { 55 | 56 | var download_args = { 57 | reportJobId : reportId, 58 | reportDownloadOptions : { 59 | exportFormat : 'CSV_EXCEL', 60 | includeReportProperties : false, 61 | includeTotalsRow : false, 62 | useGzipCompression : true 63 | } 64 | }; 65 | 66 | reportService.getReportDownloadUrlWithOptions(download_args, function (err, data) { 67 | if (err) { 68 | return console.log('ERROR', err.body); 69 | } 70 | 71 | console.log("Downloading report from " + data.rval); 72 | download_report(data.rval, 'downloaded_report.csv'); 73 | }); 74 | } 75 | 76 | if (data.rval === 'FAILED') { 77 | console.log('Report Failed!'); 78 | } 79 | 80 | if (data.rval === 'IN_PROGRESS') { 81 | setTimeout(check_report_ready, 100); 82 | } 83 | 84 | }); 85 | } 86 | 87 | reportService.runReportJob(args, function (err, jobStatus) { 88 | if (err) { 89 | return console.log('ERROR', err.body); 90 | } 91 | 92 | results = jobStatus; 93 | setTimeout(check_report_ready, 100); 94 | 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | 4 | var cfg = { 5 | pkg: grunt.file.readJSON('./package.json'), 6 | jshint: { 7 | options: { 8 | jshintrc: './.jshintrc' 9 | }, 10 | all: ['lib/**.js', 'examples/**.js', 'spec/**-spec.js'] 11 | }, 12 | jasmine_node: { 13 | options: { 14 | forceExit: true, 15 | match: '.', 16 | matchall: false, 17 | extensions: 'js', 18 | specNameMatcher: 'spec' 19 | }, 20 | all: ['spec/*spec'] 21 | } 22 | }; 23 | 24 | grunt.initConfig(cfg); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-jshint'); 27 | grunt.loadNpmTasks('grunt-jasmine-node'); 28 | 29 | var all = ['jshint', 'jasmine_node'], 30 | linters = ['jshint'], 31 | testers = ['jasmine_node']; 32 | 33 | grunt.registerTask('lint', linters); 34 | grunt.registerTask('test', testers); 35 | grunt.registerTask('all', all); 36 | }; 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/Dfp"); -------------------------------------------------------------------------------- /lib/Dfp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | User : require('./DfpUser'), 3 | Statement : require('./DfpUtils').Statement, 4 | Money : require('./DfpUtils').Money, 5 | assetByteArray: require('./DfpUtils').assetByteArray, 6 | DfpDate : require('./DfpUtils').DfpDate 7 | }; 8 | -------------------------------------------------------------------------------- /lib/DfpUser.js: -------------------------------------------------------------------------------- 1 | var DEFAULT_VERSION = 'v201802', 2 | BASE_API_URL = 'https://ads.google.com/apis/ads/publisher', 3 | {OAuth2Client} = require('google-auth-library'), 4 | packageVersion = require('../package.json').version; 5 | 6 | function DfpUser(netCode, appName, version) { 7 | this.networkCode = netCode; 8 | this.applicationName = appName; 9 | this.version = version || DEFAULT_VERSION; 10 | this.userAgent = "(DfpApi-Nodejs, ShinyAds/" + packageVersion + ", node/" + process.versions.node + ")"; 11 | 12 | return this; 13 | } 14 | 15 | DfpUser.prototype.setClient = function (client) { 16 | this.authClient = client; 17 | return this; 18 | }; 19 | 20 | DfpUser.prototype.setSettings = function (settings) { 21 | this.settings = settings; 22 | return this; 23 | }; 24 | 25 | DfpUser.prototype.getSOAPHeader = function () { 26 | return { 27 | RequestHeader: { 28 | attributes: { 29 | 'soapenv:actor' : "http://schemas.xmlsoap.org/soap/actor/next", 30 | 'soapenv:mustUnderstand' : 0, 31 | 'xsi:type' : "ns1:SoapRequestHeader", 32 | 'xmlns:ns1' : "https://www.google.com/apis/ads/publisher/" + this.version, 33 | 'xmlns:xsi' : "http://www.w3.org/2001/XMLSchema-instance", 34 | 'xmlns:soapenv' : "http://schemas.xmlsoap.org/soap/envelope/" 35 | }, 36 | 'ns1:networkCode' : this.networkCode, 37 | 'ns1:applicationName' : this.applicationName + " " + this.userAgent 38 | } 39 | }; 40 | }; 41 | 42 | DfpUser.prototype.getService = function (service, callback, version) { 43 | var soap = require('soap'); 44 | var soap_wsdl; 45 | var dfpUser = this; 46 | 47 | version = version || this.version; 48 | 49 | soap_wsdl = BASE_API_URL + '/' + version + '/' + service + '?wsdl'; 50 | 51 | var options = { 52 | ignoredNamespaces: { 53 | namespaces: ['tns'] 54 | } 55 | }; 56 | 57 | // If the callback accepts two arguments, make note that we should pass errors 58 | // to the callback. 59 | // 60 | var callbackSupportsError = callback.length === 2; 61 | 62 | this.getTokens(function (err, tokens) { 63 | 64 | if (err) { 65 | console.log('getTokens Error ' + err); 66 | var error = new Error('getTokens Error'); 67 | if (callbackSupportsError) { 68 | return callback(error); 69 | } 70 | throw error; 71 | } 72 | 73 | soap.createClient(soap_wsdl, options, function (err, client) { 74 | if (err) { 75 | console.log('Create Client Error ' + err); 76 | var error = new Error('Unable to get token'); 77 | if (callbackSupportsError) { 78 | return callback(error); 79 | } 80 | throw error; 81 | } 82 | 83 | client.addSoapHeader(dfpUser.getSOAPHeader(), '', '', ''); 84 | 85 | var serviceInstance = { 86 | soapClient: client, 87 | soapLastRequest: () => client.lastRequest 88 | }, 89 | interfacePort = client[service][service + 'InterfacePort'], 90 | invokeMethod = function (method) { 91 | return function () { 92 | var authhdr = { 'Authorization' : tokens.token_type + ' ' + tokens.access_token }; 93 | client[method](arguments[0], arguments[1], arguments[2], authhdr, arguments[3], arguments[4], arguments[5], arguments[6], arguments[7]); 94 | }; 95 | }, 96 | method, 97 | object; 98 | 99 | for (method in interfacePort) { 100 | if (interfacePort.hasOwnProperty(method)) { 101 | object = interfacePort[method]; 102 | if (object && typeof object === "function") { 103 | serviceInstance[method] = invokeMethod(method); 104 | } 105 | } 106 | } 107 | 108 | if (callbackSupportsError) { 109 | return callback(null, serviceInstance); 110 | } 111 | callback(serviceInstance); 112 | }); 113 | }); 114 | 115 | return this; 116 | }; 117 | 118 | DfpUser.prototype.getTokens = function (callback) { 119 | if (this.authClient) { 120 | return this.authClient.authorize(callback); 121 | } 122 | 123 | var oauthClient = new OAuth2Client(this.settings.client_id, this.settings.client_secret, this.settings.redirect_url); 124 | oauthClient.setCredentials({ refresh_token: this.settings.refresh_token }); 125 | oauthClient.refreshAccessToken(callback); 126 | }; 127 | 128 | module.exports = DfpUser; 129 | -------------------------------------------------------------------------------- /lib/DfpUtils.js: -------------------------------------------------------------------------------- 1 | module.exports.DfpDate = { 2 | to : function (dfpDate) { 3 | return new Date(dfpDate.date.year, dfpDate.date.month, dfpDate.date.day, dfpDate.hour, dfpDate.minute, dfpDate.second); 4 | }, 5 | from : function (today, timeZoneId, days, months) { 6 | return { 7 | date : { 8 | year : today.getFullYear(), 9 | month : today.getMonth() + 1 + (months === undefined ? 0 : months), 10 | day : today.getDate() + (days === undefined ? 0 : days) 11 | }, 12 | hour : today.getHours(), 13 | minute : today.getMinutes(), 14 | second : today.getSeconds(), 15 | timeZoneId : timeZoneId 16 | }; 17 | } 18 | }; 19 | 20 | module.exports.Statement = function (query) { 21 | return { filterStatement: { query: query } }; 22 | }; 23 | 24 | module.exports.Money = function (value, currency) { 25 | return { 26 | currencyCode: currency, 27 | microAmount: Math.round(value * 1000000) 28 | }; 29 | }; 30 | 31 | module.exports.assetByteArray = function (filename) { 32 | var fs = require('fs'), 33 | bitmap; 34 | 35 | if (!filename) { 36 | return ''; 37 | } 38 | 39 | bitmap = fs.readFileSync(filename); 40 | return new Buffer(bitmap).toString('base64'); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-google-dfp", 3 | "version": "0.3.2", 4 | "description": "Google DFP API Helper Library for NodeJS", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "grunt": "^1.0.4", 8 | "grunt-jasmine-node": "^0.3.1", 9 | "grunt-contrib-jshint": "^2.1.0", 10 | "jasmine-node": "^3.0.0" 11 | }, 12 | "dependencies": { 13 | "google-auth-library": "^1.3.2", 14 | "soap": "^0.29.0" 15 | }, 16 | "scripts": { 17 | "test": "grunt all" 18 | }, 19 | "author": "ShinyAds", 20 | "contributors": [ 21 | { 22 | "name": "Andre de Souza", 23 | "email": "ajsouz@gmail.com" 24 | }, 25 | { 26 | "name": "Gregory Pike", 27 | "email": "greg@gregpike.ca" 28 | } 29 | ], 30 | "license": "ISC", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/rubicon-project/node-google-dfp.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/rubicon-project/node-google-dfp/issues" 37 | }, 38 | "homepage": "https://github.com/rubicon-project/node-google-dfp", 39 | "keywords": [ 40 | "Google", 41 | "DFP", 42 | "DoubleClick", 43 | "adserver" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /spec/DfpUser-spec.js: -------------------------------------------------------------------------------- 1 | var DfpUser = require('../lib/DfpUser'); 2 | 3 | describe("DfpUser", function () { 4 | describe(".__construct", function () { 5 | it("should setup DFP connection", function () { 6 | var dfpSetup = new DfpUser('12345', 'myAwesomwApp', 'v201405'); 7 | 8 | expect(dfpSetup.networkCode).toBe('12345'); 9 | expect(dfpSetup.applicationName).toBe('myAwesomwApp'); 10 | expect(dfpSetup.version).toBe('v201405'); 11 | 12 | dfpSetup.setClient('Authorized Client'); 13 | expect(dfpSetup.authClient).toBe('Authorized Client'); 14 | 15 | var settingsStuct = { 16 | client_id : 'client_id', 17 | client_secret : 'client_secret', 18 | redirect_url : 'redirect_url', 19 | refresh_token : 'refresh_token' 20 | }; 21 | 22 | dfpSetup.setSettings(settingsStuct); 23 | expect(settingsStuct.client_id).toBe('client_id'); 24 | expect(settingsStuct.client_secret).toBe('client_secret'); 25 | expect(settingsStuct.redirect_url).toBe('redirect_url'); 26 | expect(settingsStuct.refresh_token).toBe('refresh_token'); 27 | 28 | var header = dfpSetup.getSOAPHeader(); 29 | expect(header.RequestHeader.attributes['soapenv:actor']).toBe('http://schemas.xmlsoap.org/soap/actor/next'); 30 | expect(header.RequestHeader.attributes['soapenv:mustUnderstand']).toBe(0); 31 | expect(header.RequestHeader.attributes['xsi:type']).toBe('ns1:SoapRequestHeader'); 32 | expect(header.RequestHeader.attributes['xmlns:ns1']).toBe('https://www.google.com/apis/ads/publisher/' + dfpSetup.version); 33 | expect(header.RequestHeader.attributes['xmlns:xsi']).toBe('http://www.w3.org/2001/XMLSchema-instance'); 34 | expect(header.RequestHeader.attributes['xmlns:soapenv']).toBe('http://schemas.xmlsoap.org/soap/envelope/'); 35 | expect(header.RequestHeader['ns1:networkCode']).toBe(dfpSetup.networkCode); 36 | expect(header.RequestHeader['ns1:applicationName']).toBe(dfpSetup.applicationName + " " + dfpSetup.userAgent); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/DfpUtils-spec.js: -------------------------------------------------------------------------------- 1 | var DfpUtils = require('../lib/DfpUtils'); 2 | 3 | describe("DfpUtils", function () { 4 | describe(".DfpDate.from", function () { 5 | it("should convert JS Date to DFP", function () { 6 | var curr_date = new Date(), 7 | dfp_date = DfpUtils.DfpDate.from(new Date(), 'America/Toronto'), 8 | result = { date : { 9 | year : curr_date.getFullYear(), 10 | month : curr_date.getMonth() + 1, 11 | day : curr_date.getDate() 12 | }, 13 | hour : curr_date.getHours(), 14 | minute : curr_date.getMinutes(), 15 | second : dfp_date.second, 16 | timeZoneID : dfp_date.timeZoneID 17 | }; 18 | 19 | expect(dfp_date.date.year).toBe(result.date.year); 20 | expect(dfp_date.date.month).toBe(result.date.month); 21 | expect(dfp_date.date.day).toBe(result.date.day); 22 | 23 | expect(dfp_date.hour).toBe(result.hour); 24 | expect(dfp_date.minute).toBe(result.minute); 25 | }); 26 | }); 27 | describe(".DfpDate.to", function () { 28 | it("should convert DFP Date to JS", function () { 29 | var curr_date = new Date(), 30 | dfp_date = { date : { 31 | year : curr_date.getFullYear(), 32 | month : curr_date.getMonth(), 33 | day : curr_date.getDate() 34 | }, 35 | hour : curr_date.getHours(), 36 | minute : curr_date.getMinutes(), 37 | second : curr_date.getSeconds(), 38 | timeZoneID : 'America/Toronto' 39 | }, 40 | js_date = DfpUtils.DfpDate.to(dfp_date); 41 | 42 | expect(js_date.getFullYear()).toBe(curr_date.getFullYear()); 43 | expect(js_date.getMonth()).toBe(curr_date.getMonth()); 44 | expect(js_date.getDate()).toBe(curr_date.getDate()); 45 | 46 | expect(js_date.getHours()).toBe(curr_date.getHours()); 47 | expect(js_date.getMinutes()).toBe(curr_date.getMinutes()); 48 | expect(js_date.getSeconds()).toBe(curr_date.getSeconds()); 49 | }); 50 | }); 51 | describe(".Statement", function () { 52 | it("should convert query to DFP Statement", function () { 53 | var dfp_statement = new DfpUtils.Statement('WHERE id > 10 LIMIT 10'), 54 | expected_query = { 55 | filterStatement : { 56 | query : 'WHERE id > 10 LIMIT 10' 57 | } 58 | }; 59 | expect(dfp_statement.filterStatement.query).toBe(expected_query.filterStatement.query); 60 | }); 61 | }); 62 | describe(".Money", function () { 63 | it("should convert money to DFP micro amounts", function () { 64 | var dfp_statement = new DfpUtils.Money(5.7, 'CAD'), 65 | expected_query = { 66 | currencyCode : 'CAD', 67 | microAmount : 5700000 68 | }; 69 | expect(dfp_statement.currencyCode).toBe(expected_query.currencyCode); 70 | expect(dfp_statement.microAmount).toBe(expected_query.microAmount); 71 | }); 72 | }); 73 | describe(".Money", function () { 74 | it("should convert money to DFP micro amounts without rounding issues", function () { 75 | var dfp_statement = new DfpUtils.Money(418/100, 'CAD'), 76 | expected_query = { 77 | currencyCode : 'CAD', 78 | microAmount : 4180000 79 | }; 80 | expect(dfp_statement.currencyCode).toBe(expected_query.currencyCode); 81 | expect(dfp_statement.microAmount).toBe(expected_query.microAmount); 82 | }); 83 | }); 84 | describe(".assetByteArray", function () { 85 | it("should convert image to assetByteArray", function () { 86 | var dfp_image_1 = DfpUtils.assetByteArray('./spec/FF4D00-0.8.png'), 87 | expected_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=", 88 | dfp_image_2 = DfpUtils.assetByteArray(''), 89 | expect_image_2 = ""; 90 | 91 | expect(dfp_image_1).toBe(expected_image); 92 | expect(dfp_image_2).toBe(expect_image_2); 93 | }); 94 | }); 95 | }); -------------------------------------------------------------------------------- /spec/FF4D00-0.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubicon-project/node-google-dfp/e2582a201a12e87272b31ca4589d54a39b26206b/spec/FF4D00-0.8.png --------------------------------------------------------------------------------