From 8a8476f0dc4d9edb246b12f26cff560e3b03478c Mon Sep 17 00:00:00 2001 From: Alan Andrade Date: Wed, 17 Aug 2016 07:40:03 -0700 Subject: [PATCH 01/24] Initial setup for google api --- package.json | 2 + quickstart.js | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 quickstart.js diff --git a/package.json b/package.json index 4a9833c..b5e7a73 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "dep": "0.0.2", + "google-auth-library": "^0.9.8", + "googleapis": "^12.2.0", "jira": "^0.9.2", "strict-mode": "^1.0.0" } diff --git a/quickstart.js b/quickstart.js new file mode 100644 index 0000000..b859825 --- /dev/null +++ b/quickstart.js @@ -0,0 +1,126 @@ + +var fs = require('fs'); +var readline = require('readline'); +var google = require('googleapis'); +var googleAuth = require('google-auth-library'); + +// If modifying these scopes, delete your previously saved credentials +// at ~/.credentials/drive-nodejs-quickstart.json +var SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly']; +var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || + process.env.USERPROFILE) + '/.credentials/'; +var TOKEN_PATH = TOKEN_DIR + 'drive-nodejs-quickstart.json'; + +// Load client secrets from a local file. +fs.readFile('client_secret.json', function processClientSecrets(err, content) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + // Authorize a client with the loaded credentials, then call the + // Drive API. + authorize(JSON.parse(content), listFiles); +}); + +/** + * Create an OAuth2 client with the given credentials, and then execute the + * given callback function. + * + * @param {Object} credentials The authorization client credentials. + * @param {function} callback The callback to call with the authorized client. + */ +function authorize(credentials, callback) { + var clientSecret = credentials.installed.client_secret; + var clientId = credentials.installed.client_id; + var redirectUrl = credentials.installed.redirect_uris[0]; + var auth = new googleAuth(); + var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); + + // Check if we have previously stored a token. + fs.readFile(TOKEN_PATH, function(err, token) { + if (err) { + getNewToken(oauth2Client, callback); + } else { + oauth2Client.credentials = JSON.parse(token); + callback(oauth2Client); + } + }); +} + +/** + * Get and store new token after prompting for user authorization, and then + * execute the given callback with the authorized OAuth2 client. + * + * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for. + * @param {getEventsCallback} callback The callback to call with the authorized + * client. + */ +function getNewToken(oauth2Client, callback) { + var authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES + }); + console.log('Authorize this app by visiting this url: ', authUrl); + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('Enter the code from that page here: ', function(code) { + rl.close(); + oauth2Client.getToken(code, function(err, token) { + if (err) { + console.log('Error while trying to retrieve access token', err); + return; + } + oauth2Client.credentials = token; + storeToken(token); + callback(oauth2Client); + }); + }); +} + +/** + * Store token to disk be used in later program executions. + * + * @param {Object} token The token to store to disk. + */ +function storeToken(token) { + try { + fs.mkdirSync(TOKEN_DIR); + } catch (err) { + if (err.code != 'EEXIST') { + throw err; + } + } + fs.writeFile(TOKEN_PATH, JSON.stringify(token)); + console.log('Token stored to ' + TOKEN_PATH); +} + +/** + * Lists the names and IDs of up to 10 files. + * + * @param {google.auth.OAuth2} auth An authorized OAuth2 client. + */ +function listFiles(auth) { + var service = google.drive('v3'); + service.files.list({ + auth: auth, + pageSize: 10, + fields: "nextPageToken, files(id, name)" + }, function(err, response) { + if (err) { + console.log('The API returned an error: ' + err); + return; + } + var files = response.files; + if (files.length == 0) { + console.log('No files found.'); + } else { + console.log('Files:'); + for (var i = 0; i < files.length; i++) { + var file = files[i]; + console.log('%s (%s)', file.name, file.id); + } + } + }); +} From 8dafaa9e9dba2fb89b239c6593b7acbcbf4944e5 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 28 Aug 2016 11:50:39 -0700 Subject: [PATCH 02/24] set up basic writing to sheet columns --- quickstart.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/quickstart.js b/quickstart.js index b859825..b38fda5 100644 --- a/quickstart.js +++ b/quickstart.js @@ -6,7 +6,11 @@ var googleAuth = require('google-auth-library'); // If modifying these scopes, delete your previously saved credentials // at ~/.credentials/drive-nodejs-quickstart.json -var SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly']; +var SCOPES = [ 'https://docs.google.com/feeds', + 'https://spreadsheets.google.com/feeds', + 'https://www.googleapis.com/auth/drive.metadata.readonly', + 'https://www.googleapis.com/auth/drive.file' + ]; var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) + '/.credentials/'; var TOKEN_PATH = TOKEN_DIR + 'drive-nodejs-quickstart.json'; @@ -101,7 +105,47 @@ function storeToken(token) { * * @param {google.auth.OAuth2} auth An authorized OAuth2 client. */ + +var serviceSheets = google.sheets('v4'); +function getSheets(auth) { + return new Promise(function(resolve, reject) { + serviceSheets.spreadsheets.get({ + auth: auth, + spreadsheetId: '1-UrXY7kbhXDV-LgAks_qEJjKfc0iOq56dPXxeYxuXtQ' + }, function(err, response) { + if (err) { reject(err) } + else { console.log(response.sheets[1]); resolve(auth, response.sheets[1]); } + }); + }); +} + +function updateSheetValue(auth, sheet) { + return new Promise(function(resolve, reject) { + serviceSheets.spreadsheets.values.append({ + auth: auth, + spreadsheetId: '1-UrXY7kbhXDV-LgAks_qEJjKfc0iOq56dPXxeYxuXtQ', + range: 'Autobot!B1:C1', + valueInputOption: 'USER_ENTERED', + resource: { + range: 'Autobot!B1:C1', + majorDimension: 'COLUMNS', + values: [ [null], ['hello'], ['world']] + } + }, function(err, response) { + if (err) { reject(err); } + else { console.log(response); resolve(response); } + }); + resolve(); + }); +} + function listFiles(auth) { + var serviceSheets = google.sheets('v4'); + + var success = function(data) { console.log('SUCCESS'); }; + var failure = function(err) { console.log('FAILURE'); console.log(err); }; + getSheets(auth).then(updateSheetValue).then(success, failure); + /* var service = google.drive('v3'); service.files.list({ auth: auth, @@ -123,4 +167,5 @@ function listFiles(auth) { } } }); + */ } From bc4076f3ad7436ac2fedcdfabc83d342cce847fb Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 14:31:06 -0700 Subject: [PATCH 03/24] add credentials for google auth and our sheets --- .gitignore | 3 ++- configs/client_secret.json.example | 14 ++++++++++++++ configs/google-sheet-oauth-token.json.example | 6 ++++++ configs/google_credentials.js.example | 1 + configs/user_mappings.js.example | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 configs/client_secret.json.example create mode 100644 configs/google-sheet-oauth-token.json.example create mode 100644 configs/google_credentials.js.example create mode 100644 configs/user_mappings.js.example diff --git a/.gitignore b/.gitignore index c3e4012..58ff4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ #Credentials -*/*_credentials.js +conifgs/*.js +configs/*.json # Logs logs diff --git a/configs/client_secret.json.example b/configs/client_secret.json.example new file mode 100644 index 0000000..e6aad60 --- /dev/null +++ b/configs/client_secret.json.example @@ -0,0 +1,14 @@ +{ + "installed": { + "client_id":"jajaja", + "project_id":"project mang", + "auth_uri":"https://accounts.google.com/o/oauth2/auth", + "token_uri":"https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", + "client_secret":"plata", + "redirect_uris": [ + "plomo", + "http://localhost" + ] + } +} diff --git a/configs/google-sheet-oauth-token.json.example b/configs/google-sheet-oauth-token.json.example new file mode 100644 index 0000000..c2c632b --- /dev/null +++ b/configs/google-sheet-oauth-token.json.example @@ -0,0 +1,6 @@ +{ + "access_token":"blah", + "token_type":"Bearer", + "refresh_token":"nice try", + "expiry_date": 00000000000000 +} diff --git a/configs/google_credentials.js.example b/configs/google_credentials.js.example new file mode 100644 index 0000000..5929652 --- /dev/null +++ b/configs/google_credentials.js.example @@ -0,0 +1 @@ +module.exports.spreadsheetId = 'you will never know'; diff --git a/configs/user_mappings.js.example b/configs/user_mappings.js.example new file mode 100644 index 0000000..43ccda1 --- /dev/null +++ b/configs/user_mappings.js.example @@ -0,0 +1,15 @@ +module.exports.nameIndexMapper = { + mang: 1, + idiota: 2, + pendejo: 3, + womang: 4, + chola: 5 +} + +module.exports.nameLetterMapper = { + mang: "A", + idiota: "B", + pendejo: "C", + womang: "D", + chola: "E" +} From ccd3aa164aff42f96bd0dc9d799a2984d0070de5 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 14:31:37 -0700 Subject: [PATCH 04/24] update packages for libraries we're trying out (async!) --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b5e7a73..3c06fa1 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,14 @@ "test": "mocha --recursive" }, "dependencies": { + "async": "^2.1.1", "dep": "0.0.2", + "fs": "0.0.1-security", "google-auth-library": "^0.9.8", "googleapis": "^12.2.0", "jira": "^0.9.2", - "strict-mode": "^1.0.0" + "readline": "^1.3.0", + "strict-mode": "^1.0.0", + "underscore": "^1.8.3" } } From 3f5f9c014dc859f2ada9965fa1e98047e11f8e36 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 14:36:11 -0700 Subject: [PATCH 05/24] add google spreadsheet resource --- main/autobot/resources/google.js | 264 +++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 main/autobot/resources/google.js diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js new file mode 100644 index 0000000..a81b867 --- /dev/null +++ b/main/autobot/resources/google.js @@ -0,0 +1,264 @@ +var googleAuth = require('google-auth-library'); +var google = require('googleapis'); +var readline = require('readline'); +var async = require('async'); +var fs = require('fs'); +var _= require('underscore'); + +nameIndexMapper = require('../../../configs/user_mappings').nameIndexMapper; +nameLetterMapper = require('../../../configs/user_mappings').nameLetterMapper; +var SPREADSHEET_ID= require('../../../configs/google_credentials').spreadsheetId; +var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; + +var TOKEN_DIR = '../../../configs/'; +var TOKEN_PATH = TOKEN_DIR + 'google-sheet-oauth-token.json'; + +var doc = google.sheets('v4'); + +function readCredentials(callback) { + fs.readFile('./configs/client_secret.json', function processClientSecrets(err, content) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + // Authorize a client with the loaded credentials, then call the + // Drive API. + var credentials = JSON.parse(content); + callback(null, credentials); + }); +}; + +function evaluateCredentials(credentials, callback) { + var clientSecret = credentials.installed.client_secret; + var clientId = credentials.installed.client_id; + var redirectUrl = credentials.installed.redirect_uris[0]; + var auth = new googleAuth(); + var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); + + callback(null, oauth2Client); +}; + +function setTokenIntoClient(oauth2Client, callback) { + fs.readFile(TOKEN_PATH, function(err, token) { + if (err) { + getNewToken(oauth2Client, callback); + } else { + oauth2Client.credentials = JSON.parse(token); + callback(null, oauth2Client); + } + }); +}; + +function getInfoFromSpreadsheet(oauth, name, callback) { + doc.spreadsheets.values.get({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + range: 'Sheet1!A2:I', + }, function(err, response) { + if(err) { + console.log(err) + } else { + var rows = response.values; + if (rows.length == 0) { console.log('NO DATA FOUND!'); } + else { + console.log('Date, %s:', name); + var userIndex = nameIndexMapper[name]; + for(var i = 0; i < rows.length; i++) { + var row = rows[i]; + console.log('%s, %s', row[0], row[userIndex]); + } + } + } + }); +}; + +function getRowsFromSpreadsheet(oauth, callback) { + doc.spreadsheets.values.get({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + range: 'Sheet1!A2:R', + }, function(err, response) { + if(err) { console.log(err) } + else { + var rows = response.values; + if (rows.length == 0) { console.log('NO DATA FOUND!'); } + else { callback(null, oauth, rows); } + } + }); +}; + +function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { + var requestBody = updateRequestBody(rows, args); + + doc.spreadsheets.values.batchUpdate({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + resource: requestBody + }, function(err, response) { + if(err) { console.log(err); } + else { + console.log(response); + callback(args); + } + }); +}; + +// *************************** HELPER FUNCTIONS ********************************* + +/** + * Get the current Date and compare it to the latest date from our spreadsheet +**/ +function isLatestDateCurrent(rows) { + var latestSheetDate = rows[rows.length -1][0]; + + return getCurrentDate() == latestSheetDate; +} + +function getCurrentDate() { + var date = new Date(); + return (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear(); +} + +function getLastRowIndex(rows) { + if(isLatestDateCurrent(rows)) { return (rows.length + 1); } + return rows.length + 2; +} + +/** + * Fetch token and then set it to disk. + * + * @param {Object} token The token to store to disk. + */ +function getNewToken(oauth2Client, callback) { + var authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES + }); + console.log('Authorize this app by visiting this url: ', authUrl); + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('Enter the code from that page here: ', function(code) { + rl.close(); + oauth2Client.getToken(code, function(err, token) { + if (err) { + console.log('Error while trying to retrieve access token', err); + return; + } + oauth2Client.credentials = token; + storeToken(token); + callback(null, oauth2Client); + }); + }); +} + +/** + * Store token to disk be used in later program executions. + * + * @param {Object} token The token to store to disk. + */ +function storeToken(token) { + try { + fs.mkdirSync(TOKEN_DIR); + } catch (err) { + if (err.code != 'EEXIST') { + throw err; + } + } + fs.writeFile(TOKEN_PATH, JSON.stringify(token)); + console.log('Token stored to ' + TOKEN_PATH); +} + + +/* + ARGs should come in the format: + { + mang: num, + mang: num, + womang: num + } +*/ +function updateRequestBody(rows, args) { + var lastRowIndex = getLastRowIndex(rows); + var dateHash = { + majorDimension: "COLUMNS", + range: "Sheet1!A" + lastRowIndex, + values: [[getCurrentDate()]] + }; + + var data = [dateHash]; + + for(var name in args) { + if(nameLetterMapper.hasOwnProperty(name)) { + var userLetter = nameLetterMapper[name]; + + userDataHash = { + majorDimension: "COLUMNS", + range: "Sheet1!" + userLetter + lastRowIndex, + values: [[args[name]]] + }; + + data.push(userDataHash); + } + } + return { valueInputOption: "USER_ENTERED", data: data }; +} + +// **************************************************************************** + +class GoogleSheet { + constructor() { } + + get(args) { + var name = args[0]; + + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getInfoFromSpreadsheet(oauth, name, callback); + } + ], + function finalCallback(err, data) { + if (err) { reject(err); } + else { resolve(data) } + }); + }); + } + update(args) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + updateRowsIntoSpreadsheet(oauth, rows, args, callback); + } + ], + function finalCallback(err, data) { + if (err) { reject(err); } + else { resolve(data) } + }); + }); + } +} + +module.exports = new GoogleSheet(); From 62cc2057fb1804abf0e47190e7da556677b7d2cb Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 14:36:38 -0700 Subject: [PATCH 06/24] utilize google spreadsheet resource in autobot --- main/autobot.js | 3 ++- main/autobot/adapters/cli.js | 25 +++++++++++++++++++++++++ main/autobot/core/core.js | 26 +++++++++++++++++++++----- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/main/autobot.js b/main/autobot.js index 4ad6ab2..a2610ab 100644 --- a/main/autobot.js +++ b/main/autobot.js @@ -3,11 +3,12 @@ var Handler = require('./autobot/handler'); var access = require('./lib/resource_accessor').access; var Adapters = require('./autobot/adapters'); +var googleCore = require('./autobot/core/core').google; class Autobot { constructor(adapter) { var adapterClass = access(Adapters, adapter); - this.adapter = new adapterClass(); + this.adapter = new adapterClass(googleCore); } receive(input) { diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js index d137426..19a047f 100644 --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -2,6 +2,28 @@ var Adapter = require('../adapters/adapter'); +function parsePlankTimes(input) { + var commandToken = input.trim().split(' ')[0]; + + var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi + var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i + + var matchedUsers = input.match(regexUsers); + var usernameGroup = 2; + var timeGroup = 5; + var parsedArgs = {}; + + for(var i = 0; i < matchedUsers.length; i++) { + var matchedUser = matchedUsers[i].match(regexUser); + var username = matchedUser[usernameGroup]; + var plankTime = matchedUser[timeGroup]; + + parsedArgs[username] = plankTime; + }; + + return { command: commandToken, args: parsedArgs }; +} + class Cli extends Adapter { /* Right now this parsing is very dumb. @@ -10,8 +32,11 @@ class Cli extends Adapter { for more intelligent mapping. */ parse(input) { + return parsePlankTimes(input); + /* var tokens = input.trim().split(' '); return { command: tokens[0], args: tokens.slice(1) } + */ } diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index ccabcc7..88ab8a8 100644 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -1,8 +1,11 @@ 'use strict'; var jiraResource = require('../resources/jira'); +var googleResource = require('../resources/google'); var access = require('../../lib/resource_accessor').access; +var doNothing = new Promise(function(resolve, reject) { resolve(); }); + class Core { constructor(commands, resource) { this.resource = resource; @@ -26,13 +29,15 @@ class Core { var commandToken = inputTokens['command']; var args = inputTokens['args']; - var cmd = access(this.commands, commandToken).bind(this); - - return cmd(args); + if(commandToken == '') { return doNothing; } + else { + var cmd = access(this.commands, commandToken).bind(this); + return cmd(args); + } } } -var commands = { +var defaultCommands = { /* A simple echo call. @@ -64,5 +69,16 @@ var commands = { } } +var googleCommands = { + get: function(args) { + return this.resource.get(args); + }, + + update: function(args) { + return this.resource.update(args); + } +} + module.exports.Core = Core; -module.exports.default = new Core(commands, jiraResource); +module.exports.default = new Core(defaultCommands, jiraResource); +module.exports.google = new Core(googleCommands, googleResource); From aa7d748533043ded95f3d81e7289ac336dca03c8 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 14:42:02 -0700 Subject: [PATCH 07/24] remove the quickstart.js tutorial --- quickstart.js | 171 -------------------------------------------------- 1 file changed, 171 deletions(-) delete mode 100644 quickstart.js diff --git a/quickstart.js b/quickstart.js deleted file mode 100644 index b38fda5..0000000 --- a/quickstart.js +++ /dev/null @@ -1,171 +0,0 @@ - -var fs = require('fs'); -var readline = require('readline'); -var google = require('googleapis'); -var googleAuth = require('google-auth-library'); - -// If modifying these scopes, delete your previously saved credentials -// at ~/.credentials/drive-nodejs-quickstart.json -var SCOPES = [ 'https://docs.google.com/feeds', - 'https://spreadsheets.google.com/feeds', - 'https://www.googleapis.com/auth/drive.metadata.readonly', - 'https://www.googleapis.com/auth/drive.file' - ]; -var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || - process.env.USERPROFILE) + '/.credentials/'; -var TOKEN_PATH = TOKEN_DIR + 'drive-nodejs-quickstart.json'; - -// Load client secrets from a local file. -fs.readFile('client_secret.json', function processClientSecrets(err, content) { - if (err) { - console.log('Error loading client secret file: ' + err); - return; - } - // Authorize a client with the loaded credentials, then call the - // Drive API. - authorize(JSON.parse(content), listFiles); -}); - -/** - * Create an OAuth2 client with the given credentials, and then execute the - * given callback function. - * - * @param {Object} credentials The authorization client credentials. - * @param {function} callback The callback to call with the authorized client. - */ -function authorize(credentials, callback) { - var clientSecret = credentials.installed.client_secret; - var clientId = credentials.installed.client_id; - var redirectUrl = credentials.installed.redirect_uris[0]; - var auth = new googleAuth(); - var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); - - // Check if we have previously stored a token. - fs.readFile(TOKEN_PATH, function(err, token) { - if (err) { - getNewToken(oauth2Client, callback); - } else { - oauth2Client.credentials = JSON.parse(token); - callback(oauth2Client); - } - }); -} - -/** - * Get and store new token after prompting for user authorization, and then - * execute the given callback with the authorized OAuth2 client. - * - * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for. - * @param {getEventsCallback} callback The callback to call with the authorized - * client. - */ -function getNewToken(oauth2Client, callback) { - var authUrl = oauth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES - }); - console.log('Authorize this app by visiting this url: ', authUrl); - var rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - rl.question('Enter the code from that page here: ', function(code) { - rl.close(); - oauth2Client.getToken(code, function(err, token) { - if (err) { - console.log('Error while trying to retrieve access token', err); - return; - } - oauth2Client.credentials = token; - storeToken(token); - callback(oauth2Client); - }); - }); -} - -/** - * Store token to disk be used in later program executions. - * - * @param {Object} token The token to store to disk. - */ -function storeToken(token) { - try { - fs.mkdirSync(TOKEN_DIR); - } catch (err) { - if (err.code != 'EEXIST') { - throw err; - } - } - fs.writeFile(TOKEN_PATH, JSON.stringify(token)); - console.log('Token stored to ' + TOKEN_PATH); -} - -/** - * Lists the names and IDs of up to 10 files. - * - * @param {google.auth.OAuth2} auth An authorized OAuth2 client. - */ - -var serviceSheets = google.sheets('v4'); -function getSheets(auth) { - return new Promise(function(resolve, reject) { - serviceSheets.spreadsheets.get({ - auth: auth, - spreadsheetId: '1-UrXY7kbhXDV-LgAks_qEJjKfc0iOq56dPXxeYxuXtQ' - }, function(err, response) { - if (err) { reject(err) } - else { console.log(response.sheets[1]); resolve(auth, response.sheets[1]); } - }); - }); -} - -function updateSheetValue(auth, sheet) { - return new Promise(function(resolve, reject) { - serviceSheets.spreadsheets.values.append({ - auth: auth, - spreadsheetId: '1-UrXY7kbhXDV-LgAks_qEJjKfc0iOq56dPXxeYxuXtQ', - range: 'Autobot!B1:C1', - valueInputOption: 'USER_ENTERED', - resource: { - range: 'Autobot!B1:C1', - majorDimension: 'COLUMNS', - values: [ [null], ['hello'], ['world']] - } - }, function(err, response) { - if (err) { reject(err); } - else { console.log(response); resolve(response); } - }); - resolve(); - }); -} - -function listFiles(auth) { - var serviceSheets = google.sheets('v4'); - - var success = function(data) { console.log('SUCCESS'); }; - var failure = function(err) { console.log('FAILURE'); console.log(err); }; - getSheets(auth).then(updateSheetValue).then(success, failure); - /* - var service = google.drive('v3'); - service.files.list({ - auth: auth, - pageSize: 10, - fields: "nextPageToken, files(id, name)" - }, function(err, response) { - if (err) { - console.log('The API returned an error: ' + err); - return; - } - var files = response.files; - if (files.length == 0) { - console.log('No files found.'); - } else { - console.log('Files:'); - for (var i = 0; i < files.length; i++) { - var file = files[i]; - console.log('%s (%s)', file.name, file.id); - } - } - }); - */ -} From 0cf8983c36a1920426d4f8b2ab8eeb547c1fad10 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 16 Oct 2016 15:11:47 -0700 Subject: [PATCH 08/24] patch up code logic to handle dynamic core types (this will fix tests) --- main/autobot.js | 6 ++++-- main/autobot/adapters/cli.js | 10 +++++----- main/autobot/core/core.js | 7 +++++++ package.json | 1 + 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/main/autobot.js b/main/autobot.js index a2610ab..6fd606b 100644 --- a/main/autobot.js +++ b/main/autobot.js @@ -4,11 +4,13 @@ var Handler = require('./autobot/handler'); var access = require('./lib/resource_accessor').access; var Adapters = require('./autobot/adapters'); var googleCore = require('./autobot/core/core').google; +var defaultCore = require('./autobot/core/core').default; class Autobot { - constructor(adapter) { + constructor(adapter, coreType = 'default') { var adapterClass = access(Adapters, adapter); - this.adapter = new adapterClass(googleCore); + if(coreType == 'google') { this.adapter = new adapterClass(googleCore); } + else { this.adapter = new adapterClass(defaultCore); } } receive(input) { diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js index 19a047f..edf3e47 100644 --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -32,11 +32,11 @@ class Cli extends Adapter { for more intelligent mapping. */ parse(input) { - return parsePlankTimes(input); - /* - var tokens = input.trim().split(' '); - return { command: tokens[0], args: tokens.slice(1) } - */ + if(this.core.type() == 'google') { return parsePlankTimes(input); } + else if(this.core.type() == 'default') { + var tokens = input.trim().split(' '); + return { command: tokens[0], args: tokens.slice(1) } + } } diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index 88ab8a8..bad9637 100644 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -35,9 +35,14 @@ class Core { return cmd(args); } } + + type() { + return access(this.commands, 'type').bind(this).call(); + } } var defaultCommands = { + type: function() { return 'default'; }, /* A simple echo call. @@ -70,6 +75,8 @@ var defaultCommands = { } var googleCommands = { + type: function() { return 'google';}, + get: function(args) { return this.resource.get(args); }, diff --git a/package.json b/package.json index 3c06fa1..beaca42 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "scripts": { "start": "node ./main/autobot/cli.js", "pretest": "rsync -av --ignore-existing ./configs/jira_credentials.js.example ./configs/jira_credentials.js", + "pretest": "rsync -av --ignore-existing ./configs/user_mappings.js.example ./configs/user_mappings.js", "test": "mocha --recursive" }, "dependencies": { From afdd038d380610099b96fdc39d980f4db19317e9 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 23 Oct 2016 17:21:35 -0700 Subject: [PATCH 09/24] replace token store to dynamo DB! --- handler.js | 6 ++- main/autobot.js | 2 +- main/autobot/adapters/slack.js | 32 ++++++++++++++-- main/autobot/cli.js | 2 +- main/autobot/resources/google.js | 37 +++++++++++++------ package.json | 2 +- serverless.yml | 63 ++++++++++++++++++++------------ 7 files changed, 101 insertions(+), 43 deletions(-) diff --git a/handler.js b/handler.js index 17f9438..9a01bcc 100644 --- a/handler.js +++ b/handler.js @@ -4,7 +4,9 @@ var qs = require('querystring'); var slackToken = require('./configs/slack_token').slackToken; var Autobot = require('./main/autobot'); -module.exports.slack = function(event, context, callback) { +module.exports.slack = function(event, context, callback) {}; + +module.exports.updateSheet = function(event, context, callback) { var statusCode, text; var body = event.body; @@ -28,7 +30,7 @@ module.exports.slack = function(event, context, callback) { context.fail("Invalid request token"); callback("Invalid request token"); } else { - var autobot = new Autobot('slack'); + var autobot = new Autobot('slack', 'google'); autobot.receive(params.text).then(success, failure); } }; diff --git a/main/autobot.js b/main/autobot.js index 6fd606b..4683d22 100644 --- a/main/autobot.js +++ b/main/autobot.js @@ -7,7 +7,7 @@ var googleCore = require('./autobot/core/core').google; var defaultCore = require('./autobot/core/core').default; class Autobot { - constructor(adapter, coreType = 'default') { + constructor(adapter, coreType) { var adapterClass = access(Adapters, adapter); if(coreType == 'google') { this.adapter = new adapterClass(googleCore); } else { this.adapter = new adapterClass(defaultCore); } diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js index 801c606..71acd88 100644 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -2,14 +2,40 @@ var Adapter = require('../adapters/adapter'); +function parsePlankTimes(input) { + var commandToken = input.trim().split(' ')[1]; + + var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi + var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i + + var matchedUsers = input.match(regexUsers); + var usernameGroup = 2; + var timeGroup = 5; + var parsedArgs = {}; + + for(var i = 0; i < matchedUsers.length; i++) { + var matchedUser = matchedUsers[i].match(regexUser); + var username = matchedUser[usernameGroup]; + var plankTime = matchedUser[timeGroup]; + + parsedArgs[username] = plankTime; + }; + + return { command: commandToken, args: parsedArgs }; +} + class Slack extends Adapter { parse(input) { - var tokens = input.split(' '); - return { command: tokens[1], args: tokens.slice(2) } + if(this.core.type() == 'google') { return parsePlankTimes(input); } + else if(this.core.type() == 'default') { + var tokens = input.trim().split(' '); + return { command: tokens[1], args: tokens.slice(2) } + } } render(data) { - return { text: JSON.stringify(data) }; + //responseData = JSON.stringify(data); + return { text: 'SUCCESS!' }; } } diff --git a/main/autobot/cli.js b/main/autobot/cli.js index a1c4416..abfe7f2 100644 --- a/main/autobot/cli.js +++ b/main/autobot/cli.js @@ -6,7 +6,7 @@ repl.start({ }); var Autobot = require('../autobot'); -var dudeBot = new Autobot('slack'); +var dudeBot = new Autobot('cli', 'google'); function evalAutobot(input, context, filename, callback) { dudeBot.receive(input).then(callback); diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index a81b867..822d121 100644 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -1,12 +1,15 @@ +'use strict'; + var googleAuth = require('google-auth-library'); var google = require('googleapis'); var readline = require('readline'); +var AWS = require('aws-sdk'); var async = require('async'); -var fs = require('fs'); +var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); var _= require('underscore'); -nameIndexMapper = require('../../../configs/user_mappings').nameIndexMapper; -nameLetterMapper = require('../../../configs/user_mappings').nameLetterMapper; +var nameIndexMapper = require('../../../configs/user_mappings').nameIndexMapper; +var nameLetterMapper = require('../../../configs/user_mappings').nameLetterMapper; var SPREADSHEET_ID= require('../../../configs/google_credentials').spreadsheetId; var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; @@ -16,14 +19,20 @@ var TOKEN_PATH = TOKEN_DIR + 'google-sheet-oauth-token.json'; var doc = google.sheets('v4'); function readCredentials(callback) { - fs.readFile('./configs/client_secret.json', function processClientSecrets(err, content) { + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-client' } }, + AttributesToGet: [ 'provider', 'value' ] + } + + dynamo.getItem(params, function processClientSecrets(err, data) { if (err) { console.log('Error loading client secret file: ' + err); return; } // Authorize a client with the loaded credentials, then call the // Drive API. - var credentials = JSON.parse(content); + var credentials = JSON.parse(data['Item']['value']['S']); callback(null, credentials); }); }; @@ -39,11 +48,17 @@ function evaluateCredentials(credentials, callback) { }; function setTokenIntoClient(oauth2Client, callback) { - fs.readFile(TOKEN_PATH, function(err, token) { - if (err) { - getNewToken(oauth2Client, callback); - } else { - oauth2Client.credentials = JSON.parse(token); + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-sheets' } }, + AttributesToGet: [ 'provider', 'token' ] + } + + dynamo.getItem(params, function(err, tokenData) { + if(err) { console.log('TOKEN IS UNAVAILABLE'); } + else { + var parsedToken = JSON.parse(tokenData['Item']['token']['S']); + oauth2Client.credentials = parsedToken; callback(null, oauth2Client); } }); @@ -193,7 +208,7 @@ function updateRequestBody(rows, args) { if(nameLetterMapper.hasOwnProperty(name)) { var userLetter = nameLetterMapper[name]; - userDataHash = { + var userDataHash = { majorDimension: "COLUMNS", range: "Sheet1!" + userLetter + lastRowIndex, values: [[args[name]]] diff --git a/package.json b/package.json index beaca42..c0c6321 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,12 @@ }, "scripts": { "start": "node ./main/autobot/cli.js", - "pretest": "rsync -av --ignore-existing ./configs/jira_credentials.js.example ./configs/jira_credentials.js", "pretest": "rsync -av --ignore-existing ./configs/user_mappings.js.example ./configs/user_mappings.js", "test": "mocha --recursive" }, "dependencies": { "async": "^2.1.1", + "aws-sdk": "^2.6.11", "dep": "0.0.2", "fs": "0.0.1-security", "google-auth-library": "^0.9.8", diff --git a/serverless.yml b/serverless.yml index f071d8c..3eba502 100644 --- a/serverless.yml +++ b/serverless.yml @@ -43,33 +43,48 @@ provider: # artifact: my-service-code.zip functions: - plank: + jira: handler: handler.slack events: - http: path: slack method: post + plank: + handler: handler.updateSheet + events: + - http: + path: plank/update + method: post -# The following are a few example events you can configure -# NOTE: Please make sure to change your handler code to work with those events -# Check the event documentation for details -# events: -# - http: -# path: users/create -# method: get -# - s3: ${env:BUCKET} -# - schedule: rate(10 minutes) -# - sns: greeter-topic -# - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 - -# you can add CloudFormation resource templates here -#resources: -# Resources: -# NewResource: -# Type: AWS::S3::Bucket -# Properties: -# BucketName: my-new-bucket -# Outputs: -# NewOutput: -# Description: "Description for the output" -# Value: "Some output value" +resources: + Resources: + DynamoDbTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: oauth + AttributeDefinitions: + - AttributeName: provider + AttributeType: S + KeySchema: + - AttributeName: provider + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + DynamoDBIamPolicy: + Type: AWS::IAM::Policy + DependsOn: DynamoDbTable + Properties: + PolicyName: lambda-dynamodb + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - dynamodb:GetItem + - dynamodb:PutItem + - dynamodb:ListTables + #Resource: arn:aws:dynamodb:*:*:table/oauth + Resource: "*" + Roles: + - Ref: IamRoleLambdaExecution From 8fa49274895d77606ad6445af62c8f10fc06c7db Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 23 Oct 2016 17:26:16 -0700 Subject: [PATCH 10/24] mbp air changes --- circle.yml | 0 configs/client_secret.json.example | 0 configs/google-sheet-oauth-token.json.example | 0 configs/google_credentials.js.example | 0 configs/jira_credentials.js.example | 0 configs/slack_token.js.example | 0 configs/user_mappings.js.example | 0 event.json | 0 handler.js | 0 main/autobot.js | 0 main/autobot/adapters.js | 0 main/autobot/adapters/adapter.js | 0 main/autobot/adapters/cli.js | 66 +++++++++++++------ main/autobot/adapters/slack.js | 0 main/autobot/cli.js | 0 main/autobot/core/core.js | 0 main/autobot/handler.js | 0 main/autobot/resources/google.js | 0 main/autobot/resources/jira.js | 0 main/lib/resource_accessor.js | 0 main/s-function.json | 0 package.json | 0 serverless.yml | 0 test/adapterTest.js | 0 test/autobotTest.js | 0 test/coreTest.js | 0 test/fixtures/drawing_fixture.js | 0 test/fixtures/parsing_fixture.js | 0 28 files changed, 45 insertions(+), 21 deletions(-) mode change 100644 => 100755 circle.yml mode change 100644 => 100755 configs/client_secret.json.example mode change 100644 => 100755 configs/google-sheet-oauth-token.json.example mode change 100644 => 100755 configs/google_credentials.js.example mode change 100644 => 100755 configs/jira_credentials.js.example mode change 100644 => 100755 configs/slack_token.js.example mode change 100644 => 100755 configs/user_mappings.js.example mode change 100644 => 100755 event.json mode change 100644 => 100755 handler.js mode change 100644 => 100755 main/autobot.js mode change 100644 => 100755 main/autobot/adapters.js mode change 100644 => 100755 main/autobot/adapters/adapter.js mode change 100644 => 100755 main/autobot/adapters/cli.js mode change 100644 => 100755 main/autobot/adapters/slack.js mode change 100644 => 100755 main/autobot/cli.js mode change 100644 => 100755 main/autobot/core/core.js mode change 100644 => 100755 main/autobot/handler.js mode change 100644 => 100755 main/autobot/resources/google.js mode change 100644 => 100755 main/autobot/resources/jira.js mode change 100644 => 100755 main/lib/resource_accessor.js mode change 100644 => 100755 main/s-function.json mode change 100644 => 100755 package.json mode change 100644 => 100755 serverless.yml mode change 100644 => 100755 test/adapterTest.js mode change 100644 => 100755 test/autobotTest.js mode change 100644 => 100755 test/coreTest.js mode change 100644 => 100755 test/fixtures/drawing_fixture.js mode change 100644 => 100755 test/fixtures/parsing_fixture.js diff --git a/circle.yml b/circle.yml old mode 100644 new mode 100755 diff --git a/configs/client_secret.json.example b/configs/client_secret.json.example old mode 100644 new mode 100755 diff --git a/configs/google-sheet-oauth-token.json.example b/configs/google-sheet-oauth-token.json.example old mode 100644 new mode 100755 diff --git a/configs/google_credentials.js.example b/configs/google_credentials.js.example old mode 100644 new mode 100755 diff --git a/configs/jira_credentials.js.example b/configs/jira_credentials.js.example old mode 100644 new mode 100755 diff --git a/configs/slack_token.js.example b/configs/slack_token.js.example old mode 100644 new mode 100755 diff --git a/configs/user_mappings.js.example b/configs/user_mappings.js.example old mode 100644 new mode 100755 diff --git a/event.json b/event.json old mode 100644 new mode 100755 diff --git a/handler.js b/handler.js old mode 100644 new mode 100755 diff --git a/main/autobot.js b/main/autobot.js old mode 100644 new mode 100755 diff --git a/main/autobot/adapters.js b/main/autobot/adapters.js old mode 100644 new mode 100755 diff --git a/main/autobot/adapters/adapter.js b/main/autobot/adapters/adapter.js old mode 100644 new mode 100755 diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js old mode 100644 new mode 100755 index edf3e47..33831dd --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -2,26 +2,54 @@ var Adapter = require('../adapters/adapter'); -function parsePlankTimes(input) { - var commandToken = input.trim().split(' ')[0]; +class Parser { + constructor(input) { + this.input = input; + } + + parse() { + if(this.input) { + var command = this.fetchCommand(); - var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi - var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i + switch(command) { + case 'update': + return this.parseUpdate(); + default: + return this.parseDefault(); + } + } + else { + return { command: '', args: {} } + } + } + + fetchCommand() { + return this.input.trim().split(' ')[0]; + } - var matchedUsers = input.match(regexUsers); - var usernameGroup = 2; - var timeGroup = 5; - var parsedArgs = {}; + parseUpdate() { + var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi + var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i - for(var i = 0; i < matchedUsers.length; i++) { - var matchedUser = matchedUsers[i].match(regexUser); - var username = matchedUser[usernameGroup]; - var plankTime = matchedUser[timeGroup]; + var matchedUsers = this.input.match(regexUsers); + var usernameGroup = 2; + var timeGroup = 5; + var parsedArgs = {}; - parsedArgs[username] = plankTime; - }; + for(var i = 0; i < matchedUsers.length; i++) { + var matchedUser = matchedUsers[i].match(regexUser); + var username = matchedUser[usernameGroup]; + var plankTime = matchedUser[timeGroup]; - return { command: commandToken, args: parsedArgs }; + parsedArgs[username] = plankTime; + }; + + return { command: 'update', args: parsedArgs }; + } + + parseDefault() { + return { command: this.fetchCommand(), args: this.input.split(' ').slice(1) } + } } class Cli extends Adapter { @@ -32,14 +60,10 @@ class Cli extends Adapter { for more intelligent mapping. */ parse(input) { - if(this.core.type() == 'google') { return parsePlankTimes(input); } - else if(this.core.type() == 'default') { - var tokens = input.trim().split(' '); - return { command: tokens[0], args: tokens.slice(1) } - } + var parser = new Parser(input) + return parser.parse(); } - /* TODO: Have an object whose responsibility it is to render this data depending on what kind of data it is? diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js old mode 100644 new mode 100755 diff --git a/main/autobot/cli.js b/main/autobot/cli.js old mode 100644 new mode 100755 diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js old mode 100644 new mode 100755 diff --git a/main/autobot/handler.js b/main/autobot/handler.js old mode 100644 new mode 100755 diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js old mode 100644 new mode 100755 diff --git a/main/autobot/resources/jira.js b/main/autobot/resources/jira.js old mode 100644 new mode 100755 diff --git a/main/lib/resource_accessor.js b/main/lib/resource_accessor.js old mode 100644 new mode 100755 diff --git a/main/s-function.json b/main/s-function.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/serverless.yml b/serverless.yml old mode 100644 new mode 100755 diff --git a/test/adapterTest.js b/test/adapterTest.js old mode 100644 new mode 100755 diff --git a/test/autobotTest.js b/test/autobotTest.js old mode 100644 new mode 100755 diff --git a/test/coreTest.js b/test/coreTest.js old mode 100644 new mode 100755 diff --git a/test/fixtures/drawing_fixture.js b/test/fixtures/drawing_fixture.js old mode 100644 new mode 100755 diff --git a/test/fixtures/parsing_fixture.js b/test/fixtures/parsing_fixture.js old mode 100644 new mode 100755 From dc44819a24096611c9199e188eccbe33fe38b4a8 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Mon, 24 Oct 2016 08:58:20 -0700 Subject: [PATCH 11/24] use all of slacks parameters instead of just using user text --- handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler.js b/handler.js index 9a01bcc..74af0ee 100755 --- a/handler.js +++ b/handler.js @@ -31,6 +31,6 @@ module.exports.updateSheet = function(event, context, callback) { callback("Invalid request token"); } else { var autobot = new Autobot('slack', 'google'); - autobot.receive(params.text).then(success, failure); + autobot.receive(params).then(success, failure); } }; From ced762fa461a7d39a77c1501c9f7d4dad7a4320f Mon Sep 17 00:00:00 2001 From: abMatGit Date: Mon, 24 Oct 2016 08:58:51 -0700 Subject: [PATCH 12/24] include greeting logic and refactor parser into object --- main/autobot/adapters/slack.js | 88 ++++++++++++++++++++++++-------- main/autobot/core/core.js | 36 ++++++++++++- main/autobot/resources/google.js | 19 +++++-- 3 files changed, 116 insertions(+), 27 deletions(-) diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js index 71acd88..e825e6f 100755 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -1,41 +1,87 @@ 'use strict'; var Adapter = require('../adapters/adapter'); +class Parser { + constructor(input) { + this.input = input.text; + this.username = input.user_name; + } + + parse() { + if(this.fetchGreeting() != null) { + return this.fetchGreeting(); + } else if(this.input) { + var command = this.fetchCommand(); + + switch(command) { + case 'update': + return this.parseUpdate(); + default: + return this.parseDefault(); + } + } else { + return { command: '', args: {} } + } + } + + fetchGreeting() { + var firstWord = this.input.trim().split(' ')[0]; + var greetingsRegex = /^(greetings|hello|hi|hey|sup)/g + + if(firstWord.match(greetingsRegex) != null) { + return { command: 'greetings', username: this.username, args: {} } + } -function parsePlankTimes(input) { - var commandToken = input.trim().split(' ')[1]; + return null; + } + + fetchCommand() { + return this.input.trim().split(' ')[1]; + } + + parseUpdate() { + var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi + var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i - var regexUsers = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/gi - var regexUser = /(@?)([a-zA-Z]*)(:?)(\ )*(\d+)/i + var matchedUsers = this.input.match(regexUsers); + var usernameGroup = 2; + var timeGroup = 5; + var parsedArgs = {}; - var matchedUsers = input.match(regexUsers); - var usernameGroup = 2; - var timeGroup = 5; - var parsedArgs = {}; + for(var i = 0; i < matchedUsers.length; i++) { + var matchedUser = matchedUsers[i].match(regexUser); + var username = matchedUser[usernameGroup]; + var plankTime = matchedUser[timeGroup]; - for(var i = 0; i < matchedUsers.length; i++) { - var matchedUser = matchedUsers[i].match(regexUser); - var username = matchedUser[usernameGroup]; - var plankTime = matchedUser[timeGroup]; + parsedArgs[username] = plankTime; + }; - parsedArgs[username] = plankTime; - }; + return { command: 'update', args: parsedArgs }; + } - return { command: commandToken, args: parsedArgs }; + parseDefault() { + return { command: this.fetchCommand(), args: this.input.split(' ').slice(2) } + } } class Slack extends Adapter { parse(input) { - if(this.core.type() == 'google') { return parsePlankTimes(input); } - else if(this.core.type() == 'default') { - var tokens = input.trim().split(' '); - return { command: tokens[1], args: tokens.slice(2) } - } + var parser = new Parser(input); + return parser.parse(); } render(data) { //responseData = JSON.stringify(data); - return { text: 'SUCCESS!' }; + //console.log(data); + if(data['totalUpdatedColumns']) { + if(data['totalUpdatedColumns'] == 2) { + return { text: 'Successfully updated 1 record!' } + } else { + return { text: 'Sucessfully updated ' + (data['totalUpdatedColumns'] - 1) + ' records!' }; + } + } else { + return { text: data } + } } } diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index bad9637..4de741f 100755 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -30,7 +30,11 @@ class Core { var args = inputTokens['args']; if(commandToken == '') { return doNothing; } - else { + else if(commandToken == 'greetings') { + var username = inputTokens['username']; + var cmd = access(this.commands, commandToken).bind(this); + return cmd(username); + } else { var cmd = access(this.commands, commandToken).bind(this); return cmd(args); } @@ -41,6 +45,14 @@ class Core { } } +// Returns a random integer between min (included) and max (excluded) +// Using Math.round() will give you a non-uniform distribution! +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; +} + var defaultCommands = { type: function() { return 'default'; }, /* @@ -77,6 +89,28 @@ var defaultCommands = { var googleCommands = { type: function() { return 'google';}, + greetings: function(username) { + var motivations = [ + " Ready to give a good plank today?", + " Good stuff today! Let's keep it going!", + " You're pretty cool, but can you plank?", + " Let's see what you can do today!" + ]; + var emojis = [ + " :sweat_drops:", + " :fuck_yes:", + " :punch:", + " :yoga:" + ]; + + var motivationIndex = getRandomInt(0,4); + var emojiIndex = getRandomInt(0,4); + return new Promise(function(resolve, reject) { + var returnString = "Hey @" + username + "!" + motivations[motivationIndex] + emojis[emojiIndex]; + resolve(returnString); + }); + }, + get: function(args) { return this.resource.get(args); }, diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index 822d121..2aa1a16 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -112,8 +112,7 @@ function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { }, function(err, response) { if(err) { console.log(err); } else { - console.log(response); - callback(args); + callback(null, response); } }); }; @@ -126,12 +125,22 @@ function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { function isLatestDateCurrent(rows) { var latestSheetDate = rows[rows.length -1][0]; - return getCurrentDate() == latestSheetDate; + if (getCurrentDate() == latestSheetDate) { + return true; + } else { + console.log("DATES DON'T MATCH!"); + console.log("Date object: %s", new Date()); + console.log("Current Date: %s", getCurrentDate()); + console.log("latestSheetDate: %s", latestSheetDate); + return false; + } } function getCurrentDate() { + var offset = 420 * 60000; // This is used for PDT timezone var date = new Date(); - return (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear(); + var offsetDate = new Date(date.getTime() - offset); + return (offsetDate.getUTCMonth() + 1) + "/" + offsetDate.getUTCDate() + "/" + offsetDate.getUTCFullYear(); } function getLastRowIndex(rows) { @@ -270,7 +279,7 @@ class GoogleSheet { ], function finalCallback(err, data) { if (err) { reject(err); } - else { resolve(data) } + else { resolve(data); } }); }); } From c8d90f0227d56a49ed6aa525e2d1c370e339f93a Mon Sep 17 00:00:00 2001 From: abMatGit Date: Mon, 24 Oct 2016 08:59:08 -0700 Subject: [PATCH 13/24] fix up test to use slacks input hash --- test/autobotTest.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/autobotTest.js b/test/autobotTest.js index 4304232..4c399ae 100755 --- a/test/autobotTest.js +++ b/test/autobotTest.js @@ -4,6 +4,7 @@ var Autobot = require('../main/autobot'); describe('Autobot', function () { context('using slack adapter', function () { var autobot = new Autobot('slack'); + var input = { text: 'autobot echo wtf', user_name: 'mang' } var failTest = function(data) { assert.equal(1,2); } var assertEcho = function(data, error) { @@ -11,7 +12,7 @@ describe('Autobot', function () { } it('uses slack adapter and the echo default command', function () { - return autobot.receive('autobot echo wtf', assertEcho); + return autobot.receive(input, assertEcho); }); }); From eb0f275ba0b4b0d02f7536651a00e5bb9bf0c2a7 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Mon, 24 Oct 2016 08:59:20 -0700 Subject: [PATCH 14/24] default to lowercase on all usernames --- main/autobot/resources/google.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index 2aa1a16..2e6595e 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -214,8 +214,9 @@ function updateRequestBody(rows, args) { var data = [dateHash]; for(var name in args) { - if(nameLetterMapper.hasOwnProperty(name)) { - var userLetter = nameLetterMapper[name]; + var username = name.toLowerCase(); + if(nameLetterMapper.hasOwnProperty(username)) { + var userLetter = nameLetterMapper[username]; var userDataHash = { majorDimension: "COLUMNS", From a15389f934dcda0a69eb344c0918bb5a2934e6d3 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Tue, 8 Nov 2016 22:52:09 -0800 Subject: [PATCH 15/24] getting some chart stuff out there --- main/autobot/resources/google.js | 105 +++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 18 deletions(-) diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index 2e6595e..0656119 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -148,6 +148,15 @@ function getLastRowIndex(rows) { return rows.length + 2; } +function getRandomColour() { + var letters = '0123456789ABCDEF'; + var color = ''; + for (var i = 0; i < 6; i++ ) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +} + /** * Fetch token and then set it to disk. * @@ -159,22 +168,6 @@ function getNewToken(oauth2Client, callback) { scope: SCOPES }); console.log('Authorize this app by visiting this url: ', authUrl); - var rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - rl.question('Enter the code from that page here: ', function(code) { - rl.close(); - oauth2Client.getToken(code, function(err, token) { - if (err) { - console.log('Error while trying to retrieve access token', err); - return; - } - oauth2Client.credentials = token; - storeToken(token); - callback(null, oauth2Client); - }); - }); } /** @@ -194,7 +187,6 @@ function storeToken(token) { console.log('Token stored to ' + TOKEN_PATH); } - /* ARGs should come in the format: { @@ -286,4 +278,81 @@ class GoogleSheet { } } -module.exports = new GoogleSheet(); +// ************************ GOOGLE CHART ******************************* +/* + This class should be initialized with chart data + { + dude: [array of values], + dudester: [array of values], + dudette: [array of values] + } +*/ +class GoogleChart { + constructor(chartData) { + this.chartData = chartData; + } + + generateChartColours() { + var colours = []; + for(var key in this.chartData) { + if(this.chartData.hasOwnProperty(key)) { + var colour = getRandomColour(); + colours.push(colour); + } + } + + var chartColoursString = "chco=" + colours.join(','); + return chartColoursString; + } + + generateChartData() { + var dataStringSet = []; + + // iterate over each set of data + for(var key in this.chartData) { + if(this.chartData.hasOwnProperty(key)) { + dataStringSet.push(this.chartData[key].join(',')); + } + } + return "chd=t:" + dataStringSet.join('%7C'); + } + + generateChartLabels() { + var chartLabels = []; + + for(var key in this.chartData) { + if(this.chartData.hasOwnProperty(key)) { + chartLabels.push(key); + } + } + + return "chdl=" + chartLabels.join('%7C'); + } + + generateChartSize() { + return "chs=700x400"; + } + + generateChartType() { + return "cht=ls" + } + + generateChartURL(){ + var chartArguments = [ + this.generateChartColours(), + this.generateChartData(), + this.generateChartLabels(), + this.generateChartSize(), + this.generateChartType() + ] + + return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); + } +} + +class GoogleURLShortener { +} + +var a = { dude: [10 ,20, 30], dudester: [20,40,60] } + +module.exports = new GoogleChart(a); From 236b3e650d53a96316e4167d228c26e1b2a0db13 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Wed, 23 Nov 2016 13:15:14 -0800 Subject: [PATCH 16/24] some small clean up --- .gitignore | 2 +- configs/client_secret.json.example | 14 -------------- configs/google-sheet-oauth-token.json.example | 6 ------ configs/slack_token.js.example | 2 +- handler.js | 11 ++++++----- main/autobot/resources/google.js | 13 ------------- 6 files changed, 8 insertions(+), 40 deletions(-) mode change 100644 => 100755 .gitignore delete mode 100755 configs/client_secret.json.example delete mode 100755 configs/google-sheet-oauth-token.json.example diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 58ff4e8..aec7e77 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ #Credentials -conifgs/*.js +configs/*.js configs/*.json # Logs diff --git a/configs/client_secret.json.example b/configs/client_secret.json.example deleted file mode 100755 index e6aad60..0000000 --- a/configs/client_secret.json.example +++ /dev/null @@ -1,14 +0,0 @@ -{ - "installed": { - "client_id":"jajaja", - "project_id":"project mang", - "auth_uri":"https://accounts.google.com/o/oauth2/auth", - "token_uri":"https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", - "client_secret":"plata", - "redirect_uris": [ - "plomo", - "http://localhost" - ] - } -} diff --git a/configs/google-sheet-oauth-token.json.example b/configs/google-sheet-oauth-token.json.example deleted file mode 100755 index c2c632b..0000000 --- a/configs/google-sheet-oauth-token.json.example +++ /dev/null @@ -1,6 +0,0 @@ -{ - "access_token":"blah", - "token_type":"Bearer", - "refresh_token":"nice try", - "expiry_date": 00000000000000 -} diff --git a/configs/slack_token.js.example b/configs/slack_token.js.example index e169767..1dcacfb 100755 --- a/configs/slack_token.js.example +++ b/configs/slack_token.js.example @@ -1 +1 @@ -module.exports.slackToken = 'blah'; +module.exports.slackTokens = ['blah', 'blah2']; diff --git a/handler.js b/handler.js index 74af0ee..6cc1088 100755 --- a/handler.js +++ b/handler.js @@ -1,8 +1,9 @@ 'use strict'; var qs = require('querystring'); -var slackToken = require('./configs/slack_token').slackToken; +var slackTokens = require('./configs/slack_token').slackTokens; var Autobot = require('./main/autobot'); +var _= require('underscore'); module.exports.slack = function(event, context, callback) {}; @@ -25,12 +26,12 @@ module.exports.updateSheet = function(event, context, callback) { context.fail(err); }; - if (requestToken !== slackToken) { + if (_.contains(slackTokens, requestToken)) { + var autobot = new Autobot('slack', 'google'); + autobot.receive(params).then(success, failure); + } else { console.error("Request token (%s) does not match exptected", requestToken); context.fail("Invalid request token"); callback("Invalid request token"); - } else { - var autobot = new Autobot('slack', 'google'); - autobot.receive(params).then(success, failure); } }; diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index 0656119..da06b4a 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -2,7 +2,6 @@ var googleAuth = require('google-auth-library'); var google = require('googleapis'); -var readline = require('readline'); var AWS = require('aws-sdk'); var async = require('async'); var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); @@ -13,9 +12,6 @@ var nameLetterMapper = require('../../../configs/user_mappings').nameLetterMappe var SPREADSHEET_ID= require('../../../configs/google_credentials').spreadsheetId; var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; -var TOKEN_DIR = '../../../configs/'; -var TOKEN_PATH = TOKEN_DIR + 'google-sheet-oauth-token.json'; - var doc = google.sheets('v4'); function readCredentials(callback) { @@ -176,15 +172,6 @@ function getNewToken(oauth2Client, callback) { * @param {Object} token The token to store to disk. */ function storeToken(token) { - try { - fs.mkdirSync(TOKEN_DIR); - } catch (err) { - if (err.code != 'EEXIST') { - throw err; - } - } - fs.writeFile(TOKEN_PATH, JSON.stringify(token)); - console.log('Token stored to ' + TOKEN_PATH); } /* From 58a06bde9e17fd3cc9cf895c6f00a242408a73fe Mon Sep 17 00:00:00 2001 From: abMatGit Date: Wed, 23 Nov 2016 13:18:03 -0800 Subject: [PATCH 17/24] formal cleanup of plankbot chart methods --- main/autobot/adapters/cli.js | 9 ++ main/autobot/adapters/slack.js | 17 ++- main/autobot/core/core.js | 4 + main/autobot/resources/google.js | 227 ++++++++++++++++++++++++++++--- 4 files changed, 239 insertions(+), 18 deletions(-) diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js index 33831dd..89fdbf8 100755 --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -14,6 +14,8 @@ class Parser { switch(command) { case 'update': return this.parseUpdate(); + case 'chart': + return this.parseChart(); default: return this.parseDefault(); } @@ -47,6 +49,13 @@ class Parser { return { command: 'update', args: parsedArgs }; } + parseChart() { + var regexUsers = /[a-zA-z]+/gi + var matchedUsers = this.input.match(regexUsers); + + return { command: 'chart', args: matchedUsers }; + } + parseDefault() { return { command: this.fetchCommand(), args: this.input.split(' ').slice(1) } } diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js index e825e6f..32e0d4f 100755 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -3,8 +3,12 @@ var Adapter = require('../adapters/adapter'); class Parser { constructor(input) { - this.input = input.text; - this.username = input.user_name; + if(input.text) { + this.input = input.text; + this.username = input.user_name; + } else { + this.input = input; + } } parse() { @@ -16,6 +20,8 @@ class Parser { switch(command) { case 'update': return this.parseUpdate(); + case 'chart': + return this.parseChart(); default: return this.parseDefault(); } @@ -59,6 +65,13 @@ class Parser { return { command: 'update', args: parsedArgs }; } + parseChart() { + var regexUsers = /[a-zA-z]+/gi + var matchedUsers = this.input.match(regexUsers); + + return { command: 'chart', args: matchedUsers }; + } + parseDefault() { return { command: this.fetchCommand(), args: this.input.split(' ').slice(2) } } diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index 4de741f..e6b9458 100755 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -115,6 +115,10 @@ var googleCommands = { return this.resource.get(args); }, + chart: function(args) { + return this.resource.chart(args); + }, + update: function(args) { return this.resource.update(args); } diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index da06b4a..d8fbf27 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -14,6 +14,9 @@ var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; var doc = google.sheets('v4'); +// --------------------------- DYNAMO CREDENTIAL READS ----------------------------- // +// + function readCredentials(callback) { var params = { TableName: 'oauth', @@ -64,7 +67,7 @@ function getInfoFromSpreadsheet(oauth, name, callback) { doc.spreadsheets.values.get({ auth: oauth, spreadsheetId: SPREADSHEET_ID, - range: 'Sheet1!A2:I', + range: 'Sheet1!A1:I', }, function(err, response) { if(err) { console.log(err) @@ -72,8 +75,8 @@ function getInfoFromSpreadsheet(oauth, name, callback) { var rows = response.values; if (rows.length == 0) { console.log('NO DATA FOUND!'); } else { - console.log('Date, %s:', name); - var userIndex = nameIndexMapper[name]; + var mapperName = name.toLowerCase(); + var userIndex = nameIndexMapper[mapperName]; for(var i = 0; i < rows.length; i++) { var row = rows[i]; console.log('%s, %s', row[0], row[userIndex]); @@ -87,7 +90,7 @@ function getRowsFromSpreadsheet(oauth, callback) { doc.spreadsheets.values.get({ auth: oauth, spreadsheetId: SPREADSHEET_ID, - range: 'Sheet1!A2:R', + range: 'Sheet1!A1:R', }, function(err, response) { if(err) { console.log(err) } else { @@ -98,6 +101,40 @@ function getRowsFromSpreadsheet(oauth, callback) { }); }; +function parseChartData(data, callback) { + var labelRow = data[0]; + var parsedChartData = {}; + var maxTime = 0; + + for(var userIndex =1; userIndex < labelRow.length; userIndex++) { + var parsedUserData = parseChartDataForUser(userIndex, data, true); + var userName = data[0][userIndex]; + + maxTime = Math.max(maxTime, parsedUserData.maxTime); + parsedChartData[userName] = parsedUserData[userName]; + } + parsedChartData.maxTime = maxTime; + + callback(null, parsedChartData); +} + +function parseChartDataWithFilters(data, filters, callback) { + var labelRow = data[0]; + var parsedChartData = {}; + var maxTime = 0; + + for(var i in filters) { + var userIndex = filters[i]; + var parsedUserData = parseChartDataForUser(userIndex, data, true); + var userName = data[0][userIndex]; + + maxTime = Math.max(maxTime, parsedUserData.maxTime); + parsedChartData[userName] = parsedUserData[userName]; + } + parsedChartData.maxTime = maxTime; + callback(null, parsedChartData); +} + function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { var requestBody = updateRequestBody(rows, args); @@ -113,6 +150,15 @@ function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { }); }; +function generateChartURL(parsedRows, callback) { + var googleChart = new GoogleChart(parsedRows); + try { + var chartURL = googleChart.generateEncodedChartURL(); + callback(null, chartURL); + } catch (err){ + console.log(err); + } +}; // *************************** HELPER FUNCTIONS ********************************* /** @@ -153,6 +199,30 @@ function getRandomColour() { return color; } +function parseChartDataForUser(userIndex, data, connected) { + var time, userData = {}, timeArray = []; + var user = data[0][userIndex]; + var lastKnownTime = undefined; + var maxTime = 0; + + for (var i =1; i < data.length; i++) { + time = data[i][userIndex]; + + // Sanitize the time if its not defined and we want to connect missing days + if(!time && lastKnownTime && connected) { time = lastKnownTime } + else if(time) { + maxTime = Math.max(maxTime, time); + lastKnownTime = time; + } + + timeArray.push(time); + } + userData[user] = timeArray; + userData['maxTime'] = maxTime; + + return userData; +} + /** * Fetch token and then set it to disk. * @@ -209,13 +279,39 @@ function updateRequestBody(rows, args) { return { valueInputOption: "USER_ENTERED", data: data }; } +/* +* This should parse out the raw data and return the array of indexes corresponding to each filter name +* Input: ['not_found', 'dude_found', 'your_mom_found'] +* Output: +* { +* not_found: undefined, +* dude_found: 1, +* your_mom_found: 13 +* } +*/ + +function extractUserIndexes(rows, filters) { + var userRows = rows[0]; + var extractedUserIndexes = []; + + for(var i = 0; i < userRows.length; i++) { + var name = userRows[i].toLowerCase(); + + if(_.contains(filters, name)) { + extractedUserIndexes.push(i); + } + } + + return extractedUserIndexes; +} + // **************************************************************************** class GoogleSheet { constructor() { } get(args) { - var name = args[0]; + var name = args[0].trim(); return new Promise(function(resolve, reject) { async.waterfall([ @@ -238,6 +334,45 @@ class GoogleSheet { }); }); } + + chart(users) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + var userFilters = extractUserIndexes(rows, users); + + if(userFilters.length > 0) { + parseChartDataWithFilters(rows, userFilters, callback); + } else { + parseChartData(rows, callback); + } + }, + function(parsedRows, callback) { + generateChartURL(parsedRows, callback); + }, + function(googleChartURL, callback) { + shortenURL(googleChartURL, callback); + } + ], + function finalCallback(err, url) { + if (err) { console.log(err); reject(err); } + else { console.log(url); resolve(url); } + }); + }); + } + update(args) { return new Promise(function(resolve, reject) { async.waterfall([ @@ -269,14 +404,19 @@ class GoogleSheet { /* This class should be initialized with chart data { + maxTime: maximum value over all user times dude: [array of values], dudester: [array of values], dudette: [array of values] - } + }, + ['dude', 'dudester', 'dudette'] */ class GoogleChart { - constructor(chartData) { + constructor(chartData, filters) { this.chartData = chartData; + this.maxTime = chartData.maxTime; + this.filters = filters; + delete chartData.maxTime; } generateChartColours() { @@ -292,18 +432,42 @@ class GoogleChart { return chartColoursString; } - generateChartData() { + generateInterpolatedChartData() { + var dataStringSet = []; + + + } + + generateEncodedChartData() { var dataStringSet = []; // iterate over each set of data for(var key in this.chartData) { if(this.chartData.hasOwnProperty(key)) { - dataStringSet.push(this.chartData[key].join(',')); + var encodedDataString = this.simpleEncode(this.chartData[key], this.maxTime); + dataStringSet.push(encodedDataString); } } - return "chd=t:" + dataStringSet.join('%7C'); + var chartData = "chd=s:" + dataStringSet.join(','); + var chartAxisRange = "chxr=0,0," + this.maxTime; + + return [chartData, chartAxisRange].join('&'); } + generatePlainChartData() { + var dataStringSet = []; + + // iterate over each set of data + for(var key in this.chartData) { + if(this.chartData.hasOwnProperty(key)) { + dataStringSet.push(this.chartData[key].join(',')); + } + } + var chartData = "chd=t:" + dataStringSet.join('%7C'); + var chartAxisRange = "chxr=0,0," + (this.maxTime * 1.2); + + return [chartData, chartAxisRange].join('&'); + } generateChartLabels() { var chartLabels = []; @@ -321,13 +485,15 @@ class GoogleChart { } generateChartType() { - return "cht=ls" + var chartType = "cht=ls"; // Chart type is a line chart + var chartAxis = "chxt=y"; // Chart axis is only the y axis + return [chartType, chartAxis].join('&'); } generateChartURL(){ var chartArguments = [ this.generateChartColours(), - this.generateChartData(), + this.generatePlainChartData(), this.generateChartLabels(), this.generateChartSize(), this.generateChartType() @@ -335,11 +501,40 @@ class GoogleChart { return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); } -} -class GoogleURLShortener { + generateEncodedChartURL(){ + var chartArguments = [ + this.generateChartColours(), + this.generateEncodedChartData(), + this.generateChartLabels(), + this.generateChartSize(), + this.generateChartType() + ] + + return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); + } + + simpleEncode(valueArray,maxValue) { + var encodedChars = []; + var simpleEncoding = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var currentValue, encodedChar; + + for (var i = 0; i < valueArray.length; i++) { + currentValue = valueArray[i]; + + if (currentValue && currentValue > 0) { + encodedChar = simpleEncoding.charAt(Math.round((simpleEncoding.length-1) * currentValue / maxValue)); + encodedChars.push(encodedChar); + } else { + encodedChars.push('_'); + } + } + return encodedChars.join(''); + } } -var a = { dude: [10 ,20, 30], dudester: [20,40,60] } +var a = { maxTime: 60, dude: [10 ,20, 30], dudester: [20,40,60] } + +//module.exports = new GoogleChart(a); +module.exports = new GoogleSheet(); -module.exports = new GoogleChart(a); From 500a36db690cd3d73b7287691cf0d5ff20a0a705 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Wed, 23 Nov 2016 13:18:49 -0800 Subject: [PATCH 18/24] add url shortening to chart method --- main/autobot/resources/google.js | 30 ++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 31 insertions(+) diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index d8fbf27..b931ecb 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -2,6 +2,8 @@ var googleAuth = require('google-auth-library'); var google = require('googleapis'); +var GoogleUrl = require('google-url'); + var AWS = require('aws-sdk'); var async = require('async'); var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); @@ -63,6 +65,27 @@ function setTokenIntoClient(oauth2Client, callback) { }); }; +function initializeGoogleUrlClient(callback) { + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-url' } }, + AttributesToGet: [ 'provider', 'key' ] + } + + dynamo.getItem(params, function(err, apiData) { + if(err) { console.log('KEY IS UNAVAILABLE'); } + else { + var parsedKey = JSON.parse(apiData['Item']['key']['S']); + var googleUrlClient = new GoogleUrl(parsedKey); + callback(null, googleUrlClient); + } + }); +}; + +// -------------------------------------------------------------------------------- + +// ********************* GOOGLE SPREADSHEET AUTH REQUESTS ************************* + function getInfoFromSpreadsheet(oauth, name, callback) { doc.spreadsheets.values.get({ auth: oauth, @@ -159,6 +182,13 @@ function generateChartURL(parsedRows, callback) { console.log(err); } }; + +function shortenURL(originalURL, callback) { + initializeGoogleUrlClient(function(err, googleUrlClient) { + googleUrlClient.shorten(originalURL, callback); + }); +}; + // *************************** HELPER FUNCTIONS ********************************* /** diff --git a/package.json b/package.json index c0c6321..38468f4 100755 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dep": "0.0.2", "fs": "0.0.1-security", "google-auth-library": "^0.9.8", + "google-url": "0.0.4", "googleapis": "^12.2.0", "jira": "^0.9.2", "readline": "^1.3.0", From 4229482897ddee127e9788d0e7a31e1e5da92452 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Wed, 23 Nov 2016 13:41:41 -0800 Subject: [PATCH 19/24] add documentation and reorganize methods --- main/autobot/resources/google.js | 127 +++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index b931ecb..ad9253a 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -124,39 +124,6 @@ function getRowsFromSpreadsheet(oauth, callback) { }); }; -function parseChartData(data, callback) { - var labelRow = data[0]; - var parsedChartData = {}; - var maxTime = 0; - - for(var userIndex =1; userIndex < labelRow.length; userIndex++) { - var parsedUserData = parseChartDataForUser(userIndex, data, true); - var userName = data[0][userIndex]; - - maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData[userName] = parsedUserData[userName]; - } - parsedChartData.maxTime = maxTime; - - callback(null, parsedChartData); -} - -function parseChartDataWithFilters(data, filters, callback) { - var labelRow = data[0]; - var parsedChartData = {}; - var maxTime = 0; - - for(var i in filters) { - var userIndex = filters[i]; - var parsedUserData = parseChartDataForUser(userIndex, data, true); - var userName = data[0][userIndex]; - - maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData[userName] = parsedUserData[userName]; - } - parsedChartData.maxTime = maxTime; - callback(null, parsedChartData); -} function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { var requestBody = updateRequestBody(rows, args); @@ -173,15 +140,6 @@ function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { }); }; -function generateChartURL(parsedRows, callback) { - var googleChart = new GoogleChart(parsedRows); - try { - var chartURL = googleChart.generateEncodedChartURL(); - callback(null, chartURL); - } catch (err){ - console.log(err); - } -}; function shortenURL(originalURL, callback) { initializeGoogleUrlClient(function(err, googleUrlClient) { @@ -220,6 +178,9 @@ function getLastRowIndex(rows) { return rows.length + 2; } +/** + * Outputs a random 6 char hex representation of a colour +**/ function getRandomColour() { var letters = '0123456789ABCDEF'; var color = ''; @@ -229,6 +190,19 @@ function getRandomColour() { return color; } + +/** + * This will generate a particular set of data points for a specific user + * If the connected flag is set to true then it will set all 'undefined' values to the previously seen value + * (which are data points missed -> person skipped that plank day) + * + * Input: + * 1) userIndex: 1 + * 2) data: [ ['Date', 'mexTaco', 'pierogi'] ['1/1/2016', 10, 20], ['1/2/2016', 30, 20]] + * 3) connected: false + * Output: + * { maxTime: 30, mexTaco: [10, 30] } +**/ function parseChartDataForUser(userIndex, data, connected) { var time, userData = {}, timeArray = []; var user = data[0][userIndex]; @@ -253,6 +227,57 @@ function parseChartDataForUser(userIndex, data, connected) { return userData; } +/** + * Will accumulate each person's data and parse it into a format that the GoogleChart object can easily render with + * Input: + * 1) data: [ ['Date', 'mexTaco', 'pierogi'] ['1/1/2016', 10, 20], ['1/2/2016', 30, 20]] + * 2) callback + * Output: + * { maxTime: 30, mexTaco: [10, 30], pierogi: [20, 20] } -> into callback +**/ +function parseChartData(data, callback) { + var labelRow = data[0]; + var parsedChartData = {}; + var maxTime = 0; + + for(var userIndex =1; userIndex < labelRow.length; userIndex++) { + var parsedUserData = parseChartDataForUser(userIndex, data, true); + var userName = data[0][userIndex]; + + maxTime = Math.max(maxTime, parsedUserData.maxTime); + parsedChartData[userName] = parsedUserData[userName]; + } + parsedChartData.maxTime = maxTime; + + callback(null, parsedChartData); +} + +/** + * Will accumulate each person's data and parse it into a format that the GoogleChart object can easily render with + * Input: + * 1) data: [ ['Date', 'mexTaco', 'pierogi', 'hotSauce'] ['1/1/2016', 10, 20, 30], ['1/2/2016', 30, 20, 40]] + * 2) filters: ['mexTaco', 'hotSauce'] + * 3) callback + * Output: + * { maxTime: 40, mexTaco: [10, 30], hotSauce: [30, 40] } -> into callback +**/ +function parseChartDataWithFilters(data, filters, callback) { + var labelRow = data[0]; + var parsedChartData = {}; + var maxTime = 0; + + for(var i in filters) { + var userIndex = filters[i]; + var parsedUserData = parseChartDataForUser(userIndex, data, true); + var userName = data[0][userIndex]; + + maxTime = Math.max(maxTime, parsedUserData.maxTime); + parsedChartData[userName] = parsedUserData[userName]; + } + parsedChartData.maxTime = maxTime; + callback(null, parsedChartData); +} + /** * Fetch token and then set it to disk. * @@ -282,6 +307,7 @@ function storeToken(token) { womang: num } */ + function updateRequestBody(rows, args) { var lastRowIndex = getLastRowIndex(rows); var dateHash = { @@ -335,6 +361,23 @@ function extractUserIndexes(rows, filters) { return extractedUserIndexes; } + +/** + * Generates the unshortened url-parameterized chart link from the parsed data + * Input: 1) { maxTime: 30, dude: [10, 20, 30], whiteMang: [10, 20, 30] } + * 2) callback + * Output: { 'http://go.og.l/1231297#whatthefucklinkisthis' } -> into callback +**/ +function generateChartURL(parsedRows, callback) { + var googleChart = new GoogleChart(parsedRows); + try { + var chartURL = googleChart.generateEncodedChartURL(); + callback(null, chartURL); + } catch (err){ + console.log(err); + } +}; + // **************************************************************************** class GoogleSheet { From 48553941937d0f8e72264220a6b44093485ccbea Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sat, 3 Dec 2016 21:28:00 -0800 Subject: [PATCH 20/24] add interpolating to plankbot ! --- main/autobot/adapters/cli.js | 9 ++ main/autobot/adapters/slack.js | 9 ++ main/autobot/core/core.js | 4 + main/autobot/resources/google.js | 153 ++++++++++++++++++++++++------- package.json | 2 +- 5 files changed, 145 insertions(+), 32 deletions(-) diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js index 89fdbf8..f440b40 100755 --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -16,6 +16,8 @@ class Parser { return this.parseUpdate(); case 'chart': return this.parseChart(); + case 'interpolate': + return this.parseInterpolate(); default: return this.parseDefault(); } @@ -56,6 +58,13 @@ class Parser { return { command: 'chart', args: matchedUsers }; } + parseInterpolate() { + var regexUsers = /([a-zA-z]+)/gi + var matchedUsers = this.input.match(regexUsers).slice(1); + + return { command: 'interpolate', args: matchedUsers }; + } + parseDefault() { return { command: this.fetchCommand(), args: this.input.split(' ').slice(1) } } diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js index 32e0d4f..8fe522b 100755 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -22,6 +22,8 @@ class Parser { return this.parseUpdate(); case 'chart': return this.parseChart(); + case 'interpolate': + return this.parseInterpolate(); default: return this.parseDefault(); } @@ -72,6 +74,13 @@ class Parser { return { command: 'chart', args: matchedUsers }; } + parseInterpolate() { + var regexUsers = /([a-zA-z]+)/gi + var matchedUsers = this.input.match(regexUsers).slice(1); + + return { command: 'interpolate', args: matchedUsers }; + } + parseDefault() { return { command: this.fetchCommand(), args: this.input.split(' ').slice(2) } } diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index e6b9458..d10c130 100755 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -119,6 +119,10 @@ var googleCommands = { return this.resource.chart(args); }, + interpolate: function(args) { + return this.resource.interpolateChart(args); + }, + update: function(args) { return this.resource.update(args); } diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js index ad9253a..928001f 100755 --- a/main/autobot/resources/google.js +++ b/main/autobot/resources/google.js @@ -8,6 +8,7 @@ var AWS = require('aws-sdk'); var async = require('async'); var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); var _= require('underscore'); +var spline = require('cubic-spline'); var nameIndexMapper = require('../../../configs/user_mappings').nameIndexMapper; var nameLetterMapper = require('../../../configs/user_mappings').nameLetterMapper; @@ -190,7 +191,6 @@ function getRandomColour() { return color; } - /** * This will generate a particular set of data points for a specific user * If the connected flag is set to true then it will set all 'undefined' values to the previously seen value @@ -201,12 +201,14 @@ function getRandomColour() { * 2) data: [ ['Date', 'mexTaco', 'pierogi'] ['1/1/2016', 10, 20], ['1/2/2016', 30, 20]] * 3) connected: false * Output: - * { maxTime: 30, mexTaco: [10, 30] } + * { maxTime: 30, mexTaco: [10, 30], lowerBound: 0, upperBound: 1} **/ function parseChartDataForUser(userIndex, data, connected) { - var time, userData = {}, timeArray = []; + var time, userData = {}, plankTimes = [], xValues = [], yValues = []; var user = data[0][userIndex]; var lastKnownTime = undefined; + var lowerBound = undefined; + var upperBound = 0; var maxTime = 0; for (var i =1; i < data.length; i++) { @@ -215,14 +217,22 @@ function parseChartDataForUser(userIndex, data, connected) { // Sanitize the time if its not defined and we want to connect missing days if(!time && lastKnownTime && connected) { time = lastKnownTime } else if(time) { + if(!lowerBound) { lowerBound = i } maxTime = Math.max(maxTime, time); lastKnownTime = time; + upperBound = i; + xValues.push(i); + yValues.push(time); } - - timeArray.push(time); + plankTimes.push(time); } - userData[user] = timeArray; + + userData['xValues'] = xValues; + userData['yValues'] = yValues; + userData['plankTimes'] = plankTimes; userData['maxTime'] = maxTime; + userData['lowerBound'] = lowerBound; + userData['upperBound'] = upperBound; return userData; } @@ -238,6 +248,7 @@ function parseChartDataForUser(userIndex, data, connected) { function parseChartData(data, callback) { var labelRow = data[0]; var parsedChartData = {}; + parsedChartData['users'] = {}; var maxTime = 0; for(var userIndex =1; userIndex < labelRow.length; userIndex++) { @@ -245,7 +256,12 @@ function parseChartData(data, callback) { var userName = data[0][userIndex]; maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData[userName] = parsedUserData[userName]; + parsedChartData['users'][userName] = {}; + parsedChartData['users'][userName]['xValues'] = parsedUserData['xValues']; + parsedChartData['users'][userName]['yValues'] = parsedUserData['yValues']; + parsedChartData['users'][userName]['plankTimes'] = parsedUserData['plankTimes']; + parsedChartData['users'][userName]['lowerBound'] = parsedUserData.lowerBound; + parsedChartData['users'][userName]['upperBound'] = parsedUserData.upperBound; } parsedChartData.maxTime = maxTime; @@ -256,25 +272,34 @@ function parseChartData(data, callback) { * Will accumulate each person's data and parse it into a format that the GoogleChart object can easily render with * Input: * 1) data: [ ['Date', 'mexTaco', 'pierogi', 'hotSauce'] ['1/1/2016', 10, 20, 30], ['1/2/2016', 30, 20, 40]] - * 2) filters: ['mexTaco', 'hotSauce'] + * 2) users: ['mexTaco', 'hotSauce'] * 3) callback * Output: - * { maxTime: 40, mexTaco: [10, 30], hotSauce: [30, 40] } -> into callback + * { maxTime: 40, { users: { mexTaco: plankTimes[10, 30], hotSauce: [30, 40] } } -> into callback **/ -function parseChartDataWithFilters(data, filters, callback) { +function parseChartDataWithUsers(data, users, callback) { var labelRow = data[0]; var parsedChartData = {}; + parsedChartData['users'] = {}; var maxTime = 0; - for(var i in filters) { - var userIndex = filters[i]; + var userIndexes = extractUserIndexes(data, users); + + for(var i in userIndexes) { + var userIndex = userIndexes[i]; var parsedUserData = parseChartDataForUser(userIndex, data, true); var userName = data[0][userIndex]; maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData[userName] = parsedUserData[userName]; + parsedChartData['users'][userName] = {}; + parsedChartData['users'][userName]['xValues'] = parsedUserData['xValues']; + parsedChartData['users'][userName]['yValues'] = parsedUserData['yValues']; + parsedChartData['users'][userName]['plankTimes'] = parsedUserData['plankTimes']; + parsedChartData['users'][userName]['lowerBound'] = parsedUserData.lowerBound; + parsedChartData['users'][userName]['upperBound'] = parsedUserData.upperBound; } parsedChartData.maxTime = maxTime; + parsedChartData.totalBound = data.length - 1; callback(null, parsedChartData); } @@ -378,6 +403,40 @@ function generateChartURL(parsedRows, callback) { } }; + +/** + * +**/ +function interpolateData(data, numUsers, callback) { + var userName; + var interpolatedData = {}; + for(var key in data.users) { + var interpolatedDataSet = []; + userName = key; + var userData = data.users[key]; + var lowerBound = userData.lowerBound; + var upperBound = userData.upperBound; + var totalBound = data.totalBound; + + var numberOfDataPoints = Math.floor(4000/numUsers); + var incrementer = totalBound / numberOfDataPoints; + + for(var x = 1; x < totalBound; x+= incrementer) { + if (x >= lowerBound && x <= upperBound) { + var y = Math.round(spline(x, userData.xValues, userData.yValues)); + interpolatedDataSet.push(y); + } else { + interpolatedDataSet.push(undefined); + } + } + interpolatedData[userName] = {}; + interpolatedData[userName]['plankTimes'] = interpolatedDataSet; + } + + interpolatedData['maxTime'] = data.maxTime; + callback(null, interpolatedData); +} + // **************************************************************************** class GoogleSheet { @@ -408,6 +467,32 @@ class GoogleSheet { }); } + update(args) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + updateRowsIntoSpreadsheet(oauth, rows, args, callback); + } + ], + function finalCallback(err, data) { + if (err) { reject(err); } + else { resolve(data); } + }); + }); + } + chart(users) { return new Promise(function(resolve, reject) { async.waterfall([ @@ -427,7 +512,7 @@ class GoogleSheet { var userFilters = extractUserIndexes(rows, users); if(userFilters.length > 0) { - parseChartDataWithFilters(rows, userFilters, callback); + parseChartDataWithUsers(rows, users, callback); } else { parseChartData(rows, callback); } @@ -446,7 +531,7 @@ class GoogleSheet { }); } - update(args) { + interpolateChart(users) { return new Promise(function(resolve, reject) { async.waterfall([ function(callback){ @@ -462,12 +547,25 @@ class GoogleSheet { getRowsFromSpreadsheet(oauth, callback); }, function(oauth, rows, callback) { - updateRowsIntoSpreadsheet(oauth, rows, args, callback); + if(users.length > 0) { + parseChartDataWithUsers(rows, users, callback); + } else { + parseChartData(rows, callback); + } + }, + function(parsedRows, callback) { + interpolateData(parsedRows, users.length, callback); + }, + function(interpolatedData, callback) { + generateChartURL(interpolatedData, callback); + }, + function(googleChartURL, callback) { + shortenURL(googleChartURL, callback); } ], - function finalCallback(err, data) { - if (err) { reject(err); } - else { resolve(data); } + function finalCallback(err, url) { + if (err) { console.log(err); reject(err); } + else { console.log(url); resolve(url); } }); }); } @@ -481,15 +579,14 @@ class GoogleSheet { dude: [array of values], dudester: [array of values], dudette: [array of values] - }, - ['dude', 'dudester', 'dudette'] + } */ class GoogleChart { - constructor(chartData, filters) { - this.chartData = chartData; + constructor(chartData) { + if (chartData.users) { this.chartData = chartData.users; } else { this.chartData = chartData } this.maxTime = chartData.maxTime; - this.filters = filters; delete chartData.maxTime; + delete chartData.totalBound; } generateChartColours() { @@ -505,19 +602,13 @@ class GoogleChart { return chartColoursString; } - generateInterpolatedChartData() { - var dataStringSet = []; - - - } - generateEncodedChartData() { var dataStringSet = []; // iterate over each set of data for(var key in this.chartData) { if(this.chartData.hasOwnProperty(key)) { - var encodedDataString = this.simpleEncode(this.chartData[key], this.maxTime); + var encodedDataString = this.simpleEncode(this.chartData[key]['plankTimes'], this.maxTime); dataStringSet.push(encodedDataString); } } diff --git a/package.json b/package.json index 38468f4..33c178e 100755 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "dependencies": { "async": "^2.1.1", "aws-sdk": "^2.6.11", + "cubic-spline": "^1.0.4", "dep": "0.0.2", "fs": "0.0.1-security", "google-auth-library": "^0.9.8", "google-url": "0.0.4", "googleapis": "^12.2.0", "jira": "^0.9.2", - "readline": "^1.3.0", "strict-mode": "^1.0.0", "underscore": "^1.8.3" } From 0c3ab09afec63a63a6a5ed98d07bcc363ded1cd6 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 4 Dec 2016 13:10:57 -0800 Subject: [PATCH 21/24] restructure google resource to have its own chart and parser for data --- main/autobot/core/core.js | 4 +- main/autobot/resources/google.js | 704 ------------------ main/autobot/resources/google/google.js | 371 +++++++++ main/autobot/resources/google/google_chart.js | 141 ++++ .../resources/google/google_sheet_parser.js | 167 +++++ 5 files changed, 681 insertions(+), 706 deletions(-) delete mode 100755 main/autobot/resources/google.js create mode 100755 main/autobot/resources/google/google.js create mode 100644 main/autobot/resources/google/google_chart.js create mode 100644 main/autobot/resources/google/google_sheet_parser.js diff --git a/main/autobot/core/core.js b/main/autobot/core/core.js index d10c130..cee5e09 100755 --- a/main/autobot/core/core.js +++ b/main/autobot/core/core.js @@ -1,7 +1,7 @@ 'use strict'; var jiraResource = require('../resources/jira'); -var googleResource = require('../resources/google'); +var googleResource = require('../resources/google/google'); var access = require('../../lib/resource_accessor').access; var doNothing = new Promise(function(resolve, reject) { resolve(); }); @@ -120,7 +120,7 @@ var googleCommands = { }, interpolate: function(args) { - return this.resource.interpolateChart(args); + return this.resource.interpolate(args); }, update: function(args) { diff --git a/main/autobot/resources/google.js b/main/autobot/resources/google.js deleted file mode 100755 index 928001f..0000000 --- a/main/autobot/resources/google.js +++ /dev/null @@ -1,704 +0,0 @@ -'use strict'; - -var googleAuth = require('google-auth-library'); -var google = require('googleapis'); -var GoogleUrl = require('google-url'); - -var AWS = require('aws-sdk'); -var async = require('async'); -var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); -var _= require('underscore'); -var spline = require('cubic-spline'); - -var nameIndexMapper = require('../../../configs/user_mappings').nameIndexMapper; -var nameLetterMapper = require('../../../configs/user_mappings').nameLetterMapper; -var SPREADSHEET_ID= require('../../../configs/google_credentials').spreadsheetId; -var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; - -var doc = google.sheets('v4'); - -// --------------------------- DYNAMO CREDENTIAL READS ----------------------------- // -// - -function readCredentials(callback) { - var params = { - TableName: 'oauth', - Key: { provider: { S: 'google-client' } }, - AttributesToGet: [ 'provider', 'value' ] - } - - dynamo.getItem(params, function processClientSecrets(err, data) { - if (err) { - console.log('Error loading client secret file: ' + err); - return; - } - // Authorize a client with the loaded credentials, then call the - // Drive API. - var credentials = JSON.parse(data['Item']['value']['S']); - callback(null, credentials); - }); -}; - -function evaluateCredentials(credentials, callback) { - var clientSecret = credentials.installed.client_secret; - var clientId = credentials.installed.client_id; - var redirectUrl = credentials.installed.redirect_uris[0]; - var auth = new googleAuth(); - var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); - - callback(null, oauth2Client); -}; - -function setTokenIntoClient(oauth2Client, callback) { - var params = { - TableName: 'oauth', - Key: { provider: { S: 'google-sheets' } }, - AttributesToGet: [ 'provider', 'token' ] - } - - dynamo.getItem(params, function(err, tokenData) { - if(err) { console.log('TOKEN IS UNAVAILABLE'); } - else { - var parsedToken = JSON.parse(tokenData['Item']['token']['S']); - oauth2Client.credentials = parsedToken; - callback(null, oauth2Client); - } - }); -}; - -function initializeGoogleUrlClient(callback) { - var params = { - TableName: 'oauth', - Key: { provider: { S: 'google-url' } }, - AttributesToGet: [ 'provider', 'key' ] - } - - dynamo.getItem(params, function(err, apiData) { - if(err) { console.log('KEY IS UNAVAILABLE'); } - else { - var parsedKey = JSON.parse(apiData['Item']['key']['S']); - var googleUrlClient = new GoogleUrl(parsedKey); - callback(null, googleUrlClient); - } - }); -}; - -// -------------------------------------------------------------------------------- - -// ********************* GOOGLE SPREADSHEET AUTH REQUESTS ************************* - -function getInfoFromSpreadsheet(oauth, name, callback) { - doc.spreadsheets.values.get({ - auth: oauth, - spreadsheetId: SPREADSHEET_ID, - range: 'Sheet1!A1:I', - }, function(err, response) { - if(err) { - console.log(err) - } else { - var rows = response.values; - if (rows.length == 0) { console.log('NO DATA FOUND!'); } - else { - var mapperName = name.toLowerCase(); - var userIndex = nameIndexMapper[mapperName]; - for(var i = 0; i < rows.length; i++) { - var row = rows[i]; - console.log('%s, %s', row[0], row[userIndex]); - } - } - } - }); -}; - -function getRowsFromSpreadsheet(oauth, callback) { - doc.spreadsheets.values.get({ - auth: oauth, - spreadsheetId: SPREADSHEET_ID, - range: 'Sheet1!A1:R', - }, function(err, response) { - if(err) { console.log(err) } - else { - var rows = response.values; - if (rows.length == 0) { console.log('NO DATA FOUND!'); } - else { callback(null, oauth, rows); } - } - }); -}; - - -function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { - var requestBody = updateRequestBody(rows, args); - - doc.spreadsheets.values.batchUpdate({ - auth: oauth, - spreadsheetId: SPREADSHEET_ID, - resource: requestBody - }, function(err, response) { - if(err) { console.log(err); } - else { - callback(null, response); - } - }); -}; - - -function shortenURL(originalURL, callback) { - initializeGoogleUrlClient(function(err, googleUrlClient) { - googleUrlClient.shorten(originalURL, callback); - }); -}; - -// *************************** HELPER FUNCTIONS ********************************* - -/** - * Get the current Date and compare it to the latest date from our spreadsheet -**/ -function isLatestDateCurrent(rows) { - var latestSheetDate = rows[rows.length -1][0]; - - if (getCurrentDate() == latestSheetDate) { - return true; - } else { - console.log("DATES DON'T MATCH!"); - console.log("Date object: %s", new Date()); - console.log("Current Date: %s", getCurrentDate()); - console.log("latestSheetDate: %s", latestSheetDate); - return false; - } -} - -function getCurrentDate() { - var offset = 420 * 60000; // This is used for PDT timezone - var date = new Date(); - var offsetDate = new Date(date.getTime() - offset); - return (offsetDate.getUTCMonth() + 1) + "/" + offsetDate.getUTCDate() + "/" + offsetDate.getUTCFullYear(); -} - -function getLastRowIndex(rows) { - if(isLatestDateCurrent(rows)) { return (rows.length + 1); } - return rows.length + 2; -} - -/** - * Outputs a random 6 char hex representation of a colour -**/ -function getRandomColour() { - var letters = '0123456789ABCDEF'; - var color = ''; - for (var i = 0; i < 6; i++ ) { - color += letters[Math.floor(Math.random() * 16)]; - } - return color; -} - -/** - * This will generate a particular set of data points for a specific user - * If the connected flag is set to true then it will set all 'undefined' values to the previously seen value - * (which are data points missed -> person skipped that plank day) - * - * Input: - * 1) userIndex: 1 - * 2) data: [ ['Date', 'mexTaco', 'pierogi'] ['1/1/2016', 10, 20], ['1/2/2016', 30, 20]] - * 3) connected: false - * Output: - * { maxTime: 30, mexTaco: [10, 30], lowerBound: 0, upperBound: 1} -**/ -function parseChartDataForUser(userIndex, data, connected) { - var time, userData = {}, plankTimes = [], xValues = [], yValues = []; - var user = data[0][userIndex]; - var lastKnownTime = undefined; - var lowerBound = undefined; - var upperBound = 0; - var maxTime = 0; - - for (var i =1; i < data.length; i++) { - time = data[i][userIndex]; - - // Sanitize the time if its not defined and we want to connect missing days - if(!time && lastKnownTime && connected) { time = lastKnownTime } - else if(time) { - if(!lowerBound) { lowerBound = i } - maxTime = Math.max(maxTime, time); - lastKnownTime = time; - upperBound = i; - xValues.push(i); - yValues.push(time); - } - plankTimes.push(time); - } - - userData['xValues'] = xValues; - userData['yValues'] = yValues; - userData['plankTimes'] = plankTimes; - userData['maxTime'] = maxTime; - userData['lowerBound'] = lowerBound; - userData['upperBound'] = upperBound; - - return userData; -} - -/** - * Will accumulate each person's data and parse it into a format that the GoogleChart object can easily render with - * Input: - * 1) data: [ ['Date', 'mexTaco', 'pierogi'] ['1/1/2016', 10, 20], ['1/2/2016', 30, 20]] - * 2) callback - * Output: - * { maxTime: 30, mexTaco: [10, 30], pierogi: [20, 20] } -> into callback -**/ -function parseChartData(data, callback) { - var labelRow = data[0]; - var parsedChartData = {}; - parsedChartData['users'] = {}; - var maxTime = 0; - - for(var userIndex =1; userIndex < labelRow.length; userIndex++) { - var parsedUserData = parseChartDataForUser(userIndex, data, true); - var userName = data[0][userIndex]; - - maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData['users'][userName] = {}; - parsedChartData['users'][userName]['xValues'] = parsedUserData['xValues']; - parsedChartData['users'][userName]['yValues'] = parsedUserData['yValues']; - parsedChartData['users'][userName]['plankTimes'] = parsedUserData['plankTimes']; - parsedChartData['users'][userName]['lowerBound'] = parsedUserData.lowerBound; - parsedChartData['users'][userName]['upperBound'] = parsedUserData.upperBound; - } - parsedChartData.maxTime = maxTime; - - callback(null, parsedChartData); -} - -/** - * Will accumulate each person's data and parse it into a format that the GoogleChart object can easily render with - * Input: - * 1) data: [ ['Date', 'mexTaco', 'pierogi', 'hotSauce'] ['1/1/2016', 10, 20, 30], ['1/2/2016', 30, 20, 40]] - * 2) users: ['mexTaco', 'hotSauce'] - * 3) callback - * Output: - * { maxTime: 40, { users: { mexTaco: plankTimes[10, 30], hotSauce: [30, 40] } } -> into callback -**/ -function parseChartDataWithUsers(data, users, callback) { - var labelRow = data[0]; - var parsedChartData = {}; - parsedChartData['users'] = {}; - var maxTime = 0; - - var userIndexes = extractUserIndexes(data, users); - - for(var i in userIndexes) { - var userIndex = userIndexes[i]; - var parsedUserData = parseChartDataForUser(userIndex, data, true); - var userName = data[0][userIndex]; - - maxTime = Math.max(maxTime, parsedUserData.maxTime); - parsedChartData['users'][userName] = {}; - parsedChartData['users'][userName]['xValues'] = parsedUserData['xValues']; - parsedChartData['users'][userName]['yValues'] = parsedUserData['yValues']; - parsedChartData['users'][userName]['plankTimes'] = parsedUserData['plankTimes']; - parsedChartData['users'][userName]['lowerBound'] = parsedUserData.lowerBound; - parsedChartData['users'][userName]['upperBound'] = parsedUserData.upperBound; - } - parsedChartData.maxTime = maxTime; - parsedChartData.totalBound = data.length - 1; - callback(null, parsedChartData); -} - -/** - * Fetch token and then set it to disk. - * - * @param {Object} token The token to store to disk. - */ -function getNewToken(oauth2Client, callback) { - var authUrl = oauth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES - }); - console.log('Authorize this app by visiting this url: ', authUrl); -} - -/** - * Store token to disk be used in later program executions. - * - * @param {Object} token The token to store to disk. - */ -function storeToken(token) { -} - -/* - ARGs should come in the format: - { - mang: num, - mang: num, - womang: num - } -*/ - -function updateRequestBody(rows, args) { - var lastRowIndex = getLastRowIndex(rows); - var dateHash = { - majorDimension: "COLUMNS", - range: "Sheet1!A" + lastRowIndex, - values: [[getCurrentDate()]] - }; - - var data = [dateHash]; - - for(var name in args) { - var username = name.toLowerCase(); - if(nameLetterMapper.hasOwnProperty(username)) { - var userLetter = nameLetterMapper[username]; - - var userDataHash = { - majorDimension: "COLUMNS", - range: "Sheet1!" + userLetter + lastRowIndex, - values: [[args[name]]] - }; - - data.push(userDataHash); - } - } - return { valueInputOption: "USER_ENTERED", data: data }; -} - -/* -* This should parse out the raw data and return the array of indexes corresponding to each filter name -* Input: ['not_found', 'dude_found', 'your_mom_found'] -* Output: -* { -* not_found: undefined, -* dude_found: 1, -* your_mom_found: 13 -* } -*/ - -function extractUserIndexes(rows, filters) { - var userRows = rows[0]; - var extractedUserIndexes = []; - - for(var i = 0; i < userRows.length; i++) { - var name = userRows[i].toLowerCase(); - - if(_.contains(filters, name)) { - extractedUserIndexes.push(i); - } - } - - return extractedUserIndexes; -} - - -/** - * Generates the unshortened url-parameterized chart link from the parsed data - * Input: 1) { maxTime: 30, dude: [10, 20, 30], whiteMang: [10, 20, 30] } - * 2) callback - * Output: { 'http://go.og.l/1231297#whatthefucklinkisthis' } -> into callback -**/ -function generateChartURL(parsedRows, callback) { - var googleChart = new GoogleChart(parsedRows); - try { - var chartURL = googleChart.generateEncodedChartURL(); - callback(null, chartURL); - } catch (err){ - console.log(err); - } -}; - - -/** - * -**/ -function interpolateData(data, numUsers, callback) { - var userName; - var interpolatedData = {}; - for(var key in data.users) { - var interpolatedDataSet = []; - userName = key; - var userData = data.users[key]; - var lowerBound = userData.lowerBound; - var upperBound = userData.upperBound; - var totalBound = data.totalBound; - - var numberOfDataPoints = Math.floor(4000/numUsers); - var incrementer = totalBound / numberOfDataPoints; - - for(var x = 1; x < totalBound; x+= incrementer) { - if (x >= lowerBound && x <= upperBound) { - var y = Math.round(spline(x, userData.xValues, userData.yValues)); - interpolatedDataSet.push(y); - } else { - interpolatedDataSet.push(undefined); - } - } - interpolatedData[userName] = {}; - interpolatedData[userName]['plankTimes'] = interpolatedDataSet; - } - - interpolatedData['maxTime'] = data.maxTime; - callback(null, interpolatedData); -} - -// **************************************************************************** - -class GoogleSheet { - constructor() { } - - get(args) { - var name = args[0].trim(); - - return new Promise(function(resolve, reject) { - async.waterfall([ - function(callback){ - readCredentials(callback); - }, - function(credentials, callback) { - evaluateCredentials(credentials, callback); - }, - function(oauth2Client, callback) { - setTokenIntoClient(oauth2Client, callback); - }, - function(oauth, callback) { - getInfoFromSpreadsheet(oauth, name, callback); - } - ], - function finalCallback(err, data) { - if (err) { reject(err); } - else { resolve(data) } - }); - }); - } - - update(args) { - return new Promise(function(resolve, reject) { - async.waterfall([ - function(callback){ - readCredentials(callback); - }, - function(credentials, callback) { - evaluateCredentials(credentials, callback); - }, - function(oauth2Client, callback) { - setTokenIntoClient(oauth2Client, callback); - }, - function(oauth, callback) { - getRowsFromSpreadsheet(oauth, callback); - }, - function(oauth, rows, callback) { - updateRowsIntoSpreadsheet(oauth, rows, args, callback); - } - ], - function finalCallback(err, data) { - if (err) { reject(err); } - else { resolve(data); } - }); - }); - } - - chart(users) { - return new Promise(function(resolve, reject) { - async.waterfall([ - function(callback){ - readCredentials(callback); - }, - function(credentials, callback) { - evaluateCredentials(credentials, callback); - }, - function(oauth2Client, callback) { - setTokenIntoClient(oauth2Client, callback); - }, - function(oauth, callback) { - getRowsFromSpreadsheet(oauth, callback); - }, - function(oauth, rows, callback) { - var userFilters = extractUserIndexes(rows, users); - - if(userFilters.length > 0) { - parseChartDataWithUsers(rows, users, callback); - } else { - parseChartData(rows, callback); - } - }, - function(parsedRows, callback) { - generateChartURL(parsedRows, callback); - }, - function(googleChartURL, callback) { - shortenURL(googleChartURL, callback); - } - ], - function finalCallback(err, url) { - if (err) { console.log(err); reject(err); } - else { console.log(url); resolve(url); } - }); - }); - } - - interpolateChart(users) { - return new Promise(function(resolve, reject) { - async.waterfall([ - function(callback){ - readCredentials(callback); - }, - function(credentials, callback) { - evaluateCredentials(credentials, callback); - }, - function(oauth2Client, callback) { - setTokenIntoClient(oauth2Client, callback); - }, - function(oauth, callback) { - getRowsFromSpreadsheet(oauth, callback); - }, - function(oauth, rows, callback) { - if(users.length > 0) { - parseChartDataWithUsers(rows, users, callback); - } else { - parseChartData(rows, callback); - } - }, - function(parsedRows, callback) { - interpolateData(parsedRows, users.length, callback); - }, - function(interpolatedData, callback) { - generateChartURL(interpolatedData, callback); - }, - function(googleChartURL, callback) { - shortenURL(googleChartURL, callback); - } - ], - function finalCallback(err, url) { - if (err) { console.log(err); reject(err); } - else { console.log(url); resolve(url); } - }); - }); - } -} - -// ************************ GOOGLE CHART ******************************* -/* - This class should be initialized with chart data - { - maxTime: maximum value over all user times - dude: [array of values], - dudester: [array of values], - dudette: [array of values] - } -*/ -class GoogleChart { - constructor(chartData) { - if (chartData.users) { this.chartData = chartData.users; } else { this.chartData = chartData } - this.maxTime = chartData.maxTime; - delete chartData.maxTime; - delete chartData.totalBound; - } - - generateChartColours() { - var colours = []; - for(var key in this.chartData) { - if(this.chartData.hasOwnProperty(key)) { - var colour = getRandomColour(); - colours.push(colour); - } - } - - var chartColoursString = "chco=" + colours.join(','); - return chartColoursString; - } - - generateEncodedChartData() { - var dataStringSet = []; - - // iterate over each set of data - for(var key in this.chartData) { - if(this.chartData.hasOwnProperty(key)) { - var encodedDataString = this.simpleEncode(this.chartData[key]['plankTimes'], this.maxTime); - dataStringSet.push(encodedDataString); - } - } - var chartData = "chd=s:" + dataStringSet.join(','); - var chartAxisRange = "chxr=0,0," + this.maxTime; - - return [chartData, chartAxisRange].join('&'); - } - - generatePlainChartData() { - var dataStringSet = []; - - // iterate over each set of data - for(var key in this.chartData) { - if(this.chartData.hasOwnProperty(key)) { - dataStringSet.push(this.chartData[key].join(',')); - } - } - var chartData = "chd=t:" + dataStringSet.join('%7C'); - var chartAxisRange = "chxr=0,0," + (this.maxTime * 1.2); - - return [chartData, chartAxisRange].join('&'); - } - generateChartLabels() { - var chartLabels = []; - - for(var key in this.chartData) { - if(this.chartData.hasOwnProperty(key)) { - chartLabels.push(key); - } - } - - return "chdl=" + chartLabels.join('%7C'); - } - - generateChartSize() { - return "chs=700x400"; - } - - generateChartType() { - var chartType = "cht=ls"; // Chart type is a line chart - var chartAxis = "chxt=y"; // Chart axis is only the y axis - return [chartType, chartAxis].join('&'); - } - - generateChartURL(){ - var chartArguments = [ - this.generateChartColours(), - this.generatePlainChartData(), - this.generateChartLabels(), - this.generateChartSize(), - this.generateChartType() - ] - - return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); - } - - generateEncodedChartURL(){ - var chartArguments = [ - this.generateChartColours(), - this.generateEncodedChartData(), - this.generateChartLabels(), - this.generateChartSize(), - this.generateChartType() - ] - - return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); - } - - simpleEncode(valueArray,maxValue) { - var encodedChars = []; - var simpleEncoding = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - var currentValue, encodedChar; - - for (var i = 0; i < valueArray.length; i++) { - currentValue = valueArray[i]; - - if (currentValue && currentValue > 0) { - encodedChar = simpleEncoding.charAt(Math.round((simpleEncoding.length-1) * currentValue / maxValue)); - encodedChars.push(encodedChar); - } else { - encodedChars.push('_'); - } - } - return encodedChars.join(''); - } -} - -var a = { maxTime: 60, dude: [10 ,20, 30], dudester: [20,40,60] } - -//module.exports = new GoogleChart(a); -module.exports = new GoogleSheet(); - diff --git a/main/autobot/resources/google/google.js b/main/autobot/resources/google/google.js new file mode 100755 index 0000000..24fabab --- /dev/null +++ b/main/autobot/resources/google/google.js @@ -0,0 +1,371 @@ +'use strict'; + +var googleAuth = require('google-auth-library'); +var google = require('googleapis'); +var GoogleUrl = require('google-url'); +var GoogleChart = require('./google_chart'); +var GoogleSheetParser = require('./google_sheet_parser'); + +var AWS = require('aws-sdk'); +var async = require('async'); +var dynamo = new AWS.DynamoDB({ region: 'us-east-1' }); + +var nameIndexMapper = require('../../../../configs/user_mappings').nameIndexMapper; +var nameLetterMapper = require('../../../../configs/user_mappings').nameLetterMapper; +var SPREADSHEET_ID= require('../../../../configs/google_credentials').spreadsheetId; +var SCOPES = [ 'https://www.googleapis.com/auth/spreadsheets' ]; + +var doc = google.sheets('v4'); + +// --------------------------- DYNAMO CREDENTIAL READS ----------------------------- // +// + +function readCredentials(callback) { + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-client' } }, + AttributesToGet: [ 'provider', 'value' ] + } + + dynamo.getItem(params, function processClientSecrets(err, data) { + if (err) { + console.log('Error loading client secret file: ' + err); + return; + } + // Authorize a client with the loaded credentials, then call the + // Drive API. + var credentials = JSON.parse(data['Item']['value']['S']); + callback(null, credentials); + }); +}; + +function evaluateCredentials(credentials, callback) { + var clientSecret = credentials.installed.client_secret; + var clientId = credentials.installed.client_id; + var redirectUrl = credentials.installed.redirect_uris[0]; + var auth = new googleAuth(); + var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); + + callback(null, oauth2Client); +}; + +function setTokenIntoClient(oauth2Client, callback) { + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-sheets' } }, + AttributesToGet: [ 'provider', 'token' ] + } + + dynamo.getItem(params, function(err, tokenData) { + if(err) { console.log('TOKEN IS UNAVAILABLE'); } + else { + var parsedToken = JSON.parse(tokenData['Item']['token']['S']); + oauth2Client.credentials = parsedToken; + callback(null, oauth2Client); + } + }); +}; + +function shortenURL(url, callback) { + var params = { + TableName: 'oauth', + Key: { provider: { S: 'google-url' } }, + AttributesToGet: [ 'provider', 'key' ] + } + + dynamo.getItem(params, function(err, apiData) { + if(err) { console.log('KEY IS UNAVAILABLE'); } + else { + var parsedKey = JSON.parse(apiData['Item']['key']['S']); + var googleUrlClient = new GoogleUrl(parsedKey); + googleUrlClient.shorten(url, callback); + } + }); +}; + +// -------------------------------------------------------------------------------- + +// ********************* GOOGLE SPREADSHEET AUTH REQUESTS ************************* + +function getInfoFromSpreadsheet(oauth, name, callback) { + doc.spreadsheets.values.get({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + range: 'Sheet1!A1:I', + }, function(err, response) { + if(err) { + console.log(err) + } else { + var rows = response.values; + if (rows.length == 0) { console.log('NO DATA FOUND!'); } + else { + var mapperName = name.toLowerCase(); + var userIndex = nameIndexMapper[mapperName]; + for(var i = 0; i < rows.length; i++) { + var row = rows[i]; + console.log('%s, %s', row[0], row[userIndex]); + } + } + } + }); +}; + +function getRowsFromSpreadsheet(oauth, callback) { + doc.spreadsheets.values.get({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + range: 'Sheet1!A1:R', + }, function(err, response) { + if(err) { console.log(err) } + else { + var rows = response.values; + if (rows.length == 0) { console.log('NO DATA FOUND!'); } + else { callback(null, oauth, rows); } + } + }); +}; + + +function updateRowsIntoSpreadsheet(oauth, rows, args, callback) { + var requestBody = updateRequestBody(rows, args); + + doc.spreadsheets.values.batchUpdate({ + auth: oauth, + spreadsheetId: SPREADSHEET_ID, + resource: requestBody + }, function(err, response) { + if(err) { console.log(err); } + else { + callback(null, response); + } + }); +}; + +// *************************** HELPER FUNCTIONS ********************************* + +/** + * Get the current Date and compare it to the latest date from our spreadsheet +**/ +function isLatestDateCurrent(rows) { + var latestSheetDate = rows[rows.length -1][0]; + + if (getCurrentDate() == latestSheetDate) { + return true; + } else { + console.log("DATES DON'T MATCH!"); + console.log("Date object: %s", new Date()); + console.log("Current Date: %s", getCurrentDate()); + console.log("latestSheetDate: %s", latestSheetDate); + return false; + } +} + +function getCurrentDate() { + var offset = 420 * 60000; // This is used for PDT timezone + var date = new Date(); + var offsetDate = new Date(date.getTime() - offset); + return (offsetDate.getUTCMonth() + 1) + "/" + offsetDate.getUTCDate() + "/" + offsetDate.getUTCFullYear(); +} + +function getLastRowIndex(rows) { + if(isLatestDateCurrent(rows)) { return (rows.length + 1); } + return rows.length + 2; +} + +/** + * Fetch token and then set it to disk. + * + * @param {Object} token The token to store to disk. + */ +function getNewToken(oauth2Client, callback) { + var authUrl = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES + }); + console.log('Authorize this app by visiting this url: ', authUrl); +} + +/** + * Store token to disk be used in later program executions. + * + * @param {Object} token The token to store to disk. + */ +function storeToken(token) { +} + +/* + ARGs should come in the format: + { + mang: num, + mang: num, + womang: num + } +*/ + +function updateRequestBody(rows, args) { + var lastRowIndex = getLastRowIndex(rows); + var dateHash = { + majorDimension: "COLUMNS", + range: "Sheet1!A" + lastRowIndex, + values: [[getCurrentDate()]] + }; + + var data = [dateHash]; + + for(var name in args) { + var username = name.toLowerCase(); + if(nameLetterMapper.hasOwnProperty(username)) { + var userLetter = nameLetterMapper[username]; + + var userDataHash = { + majorDimension: "COLUMNS", + range: "Sheet1!" + userLetter + lastRowIndex, + values: [[args[name]]] + }; + + data.push(userDataHash); + } + } + return { valueInputOption: "USER_ENTERED", data: data }; +} + +/** + * Generates the unshortened url-parameterized chart link from the parsed data + * Input: 1) { maxTime: 30, dude: [10, 20, 30], whiteMang: [10, 20, 30] } + * 2) callback + * Output: { 'http://go.og.l/1231297#whatthefucklinkisthis' } -> into callback +**/ +function generateChart(rows, users, options, callback) { + var data = { filters: users, rows: rows }; + var googleSheetParser = new GoogleSheetParser(data); + + var parsedData = googleSheetParser.parse(options); + var googleChart = new GoogleChart(parsedData); + + try { + var chartURL = googleChart.generateEncodedChartURL(); + callback(null, chartURL); + } catch (err){ + console.log(err); + } +}; + +// **************************************************************************** + +class GoogleSheet { + constructor() { } + + get(args) { + var name = args[0].trim(); + + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getInfoFromSpreadsheet(oauth, name, callback); + } + ], + function finalCallback(err, data) { + if (err) { reject(err); } + else { resolve(data) } + }); + }); + } + + update(args) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + updateRowsIntoSpreadsheet(oauth, rows, args, callback); + } + ], + function finalCallback(err, data) { + if (err) { reject(err); } + else { resolve(data); } + }); + }); + } + + chart(users) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + var options = { interpolate: false }; + generateChart(rows, users, options, callback); + }, + function(googleChartURL, callback) { + shortenURL(googleChartURL, callback); + } + ], + function finalCallback(err, url) { + if (err) { console.log(err); reject(err); } + else { console.log(url); resolve(url); } + }); + }); + } + + interpolate(users) { + return new Promise(function(resolve, reject) { + async.waterfall([ + function(callback){ + readCredentials(callback); + }, + function(credentials, callback) { + evaluateCredentials(credentials, callback); + }, + function(oauth2Client, callback) { + setTokenIntoClient(oauth2Client, callback); + }, + function(oauth, callback) { + getRowsFromSpreadsheet(oauth, callback); + }, + function(oauth, rows, callback) { + var options = { interpolate: true }; + generateChart(rows, users, options, callback); + }, + function(googleChartURL, callback) { + shortenURL(googleChartURL, callback); + } + ], + function finalCallback(err, url) { + if (err) { console.log(err); reject(err); } + else { console.log(url); resolve(url); } + }); + }); + } +} + +module.exports = new GoogleSheet(); + diff --git a/main/autobot/resources/google/google_chart.js b/main/autobot/resources/google/google_chart.js new file mode 100644 index 0000000..d3036d6 --- /dev/null +++ b/main/autobot/resources/google/google_chart.js @@ -0,0 +1,141 @@ +// ************************ GOOGLE CHART ******************************* +/* + This class should be initialized with chart data + { + totalMaxTime: maximum value over all user times + users: { + dude: [array of values], + dudester: [array of values], + dudette: [array of values] + } + } +*/ +class GoogleChart { + constructor(data) { + this.users = data.users; + this.totalMaxTime = data.totalMaxTime; + } + + generateChartColours() { + var colours = []; + for(var user in this.users) { + if(this.users.hasOwnProperty(user)) { + var colour = this.getRandomColour(); + colours.push(colour); + } + } + + var chartColoursString = "chco=" + colours.join(','); + return chartColoursString; + } + + generateEncodedChartData() { + var dataStringSet = []; + + // iterate over each set of data + for(var user in this.users) { + if(this.users.hasOwnProperty(user)) { + var values = this.users[user]; + var encodedDataString = this.simpleEncode(values, this.totalMaxTime); + dataStringSet.push(encodedDataString); + } + } + var chartData = "chd=s:" + dataStringSet.join(','); + var chartAxisRange = "chxr=0,0," + this.totalMaxTime; + + return [chartData, chartAxisRange].join('&'); + } + + generatePlainChartData() { + var dataStringSet = []; + + // iterate over each set of data + for(var user in this.users) { + if(this.users.hasOwnProperty(user)) { + dataStringSet.push(this.users[user].join(',')); + } + } + var chartData = "chd=t:" + dataStringSet.join('%7C'); + var chartAxisRange = "chxr=0,0," + (this.maxTime * 1.2); + + return [chartData, chartAxisRange].join('&'); + } + generateChartLabels() { + var chartLabels = []; + + for(var user in this.users) { + if(this.users.hasOwnProperty(user)) { + chartLabels.push(user); + } + } + + return "chdl=" + chartLabels.join('%7C'); + } + + generateChartSize() { + return "chs=700x400"; + } + + generateChartType() { + var chartType = "cht=ls"; // Chart type is a line chart + var chartAxis = "chxt=y"; // Chart axis is only the y axis + return [chartType, chartAxis].join('&'); + } + + generateChartURL(){ + var chartArguments = [ + this.generateChartColours(), + this.generatePlainChartData(), + this.generateChartLabels(), + this.generateChartSize(), + this.generateChartType() + ] + + return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); + } + + generateEncodedChartURL(){ + var chartArguments = [ + this.generateChartColours(), + this.generateEncodedChartData(), + this.generateChartLabels(), + this.generateChartSize(), + this.generateChartType() + ] + + return "https://chart.apis.google.com/chart?" + chartArguments.join('&'); + } + + // ************************* HELPER FUNCTIONS *************************** + + simpleEncode(valueArray,maxValue) { + var encodedChars = []; + var simpleEncoding = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var currentValue, encodedChar; + + for (var i = 0; i < valueArray.length; i++) { + currentValue = valueArray[i]; + + if (currentValue && currentValue > 0) { + encodedChar = simpleEncoding.charAt(Math.round((simpleEncoding.length-1) * currentValue / maxValue)); + encodedChars.push(encodedChar); + } else { + encodedChars.push('_'); + } + } + return encodedChars.join(''); + } + + /** + * Outputs a random 6 char hex representation of a colour + **/ + getRandomColour(){ + var letters = '0123456789ABCDEF'; + var color = ''; + for (var i = 0; i < 6; i++ ) { color += letters[Math.floor(Math.random() * 16)]; } + return color; + } +} + +var a = { maxTime: 60, dude: [10 ,20, 30], dudester: [20,40,60] } +module.exports = GoogleChart diff --git a/main/autobot/resources/google/google_sheet_parser.js b/main/autobot/resources/google/google_sheet_parser.js new file mode 100644 index 0000000..8614513 --- /dev/null +++ b/main/autobot/resources/google/google_sheet_parser.js @@ -0,0 +1,167 @@ +var _= require('underscore'); +var spline = require('cubic-spline'); + +class GoogleSheetParser { + constructor(data) { + this.data = data; + if(this.data.filters.length == 0) { + this.users = this.extractUsers([], true); + } else { + this.users = this.extractUsers(this.data.filters, false); + } + } + + /** + * Output: + * { + * totalMaxTime: 40, + * users: { mexTaco: , pierogi: , hotSauce: }, + * totalBound: 4 + * } + **/ + parse(options) { + var parsedData = { users: {} }; + var totalMaxTime = 0; + var users = []; + var connected = !options.interpolate + var extractAllUsers = (this.data.filters.length == 0); + + // if our filters length is zero then we parse all user data + users = this.extractUsers(this.data.filters, extractAllUsers); + + var totalBound = this.data.rows.length -1; + var numUsers = users.length; + + for(var i in users) { + var user = users[i]; + var userData = this.generateDataForUser(user, connected); + if (options.interpolate) { userData = this.interpolateData(userData, numUsers, totalBound); } + + parsedData.users[user.name] = userData.timesToChart; + totalMaxTime = Math.max(totalMaxTime, userData.maxTime); + } + + parsedData.totalMaxTime = totalMaxTime; + + return parsedData; + } + + /** + * Input: + * userData: + * numUsers: 2 + * totalBound: 30 + * Output: + * userData: + * { + * ... + * timesToChart: [ interpolated cubic spline data for the user ] + * } + **/ + interpolateData(userData, numUsers, totalBound) { + var interpolatedData = []; + var numberOfDataPoints = Math.floor(1900/numUsers); + var incrementer = totalBound / numberOfDataPoints; + + for(var x = 1; x < totalBound; x+= incrementer) { + if( x>= userData.lowerXBound && x <= userData.upperXBound ) { + var y = Math.round(spline(x, userData.xValues, userData.yValues)); + interpolatedData.push(y); + } else { + // If user is not within the domain range, he does not have a value + interpolatedData.push(undefined); + } + } + + userData['timesToChart'] = interpolatedData; + return userData; + } + + /* + * This should parse out the raw data and return the array of indexes corresponding to each filter name + * Input: + * filters: ['not_found', 'dude_found', 'your_mom_found'] + * extractAllUsers: false + * Output: + * [ + * { name: dude_found, index: 1 }, + * { name: your_mom_found, index: 13 } + * ] + */ + extractUsers(filters, extractAllUsers) { + var userRows = this.data.rows[0]; + var extractedUsers = []; + + for(var i = 0; i < userRows.length; i++) { + var name = userRows[i].toLowerCase(); + var user = {}; + + if(extractAllUsers || _.contains(filters, name)) { + user.name = name; + user.index = i; + extractedUsers.push(user); + } + } + + return extractedUsers; + } + + /** + * Input: + * user: { name: taco, index: 1 } + * connected: true + * Output: + * { + * name: taco, + * points: [{ x: 2, y: 3 }, { x: 3, y: 4 }, { x: 4, y: 6 }], + * xValues: [ 2, 3, 4], + * yValues: [ 3, 4, 6], + * timesToChart: [undefined, 3, 4, 6, 6, 6], <~ last 2 sixes are 'connected' + * maxTime: 6, + * lowerXBound: 2, + * upperXBound: 4 + * } + **/ + generateDataForUser(user, connected) { + var points = [], xValues = [], yValues = []; + var timesToChart = []; + var userData = {}; + + var time = undefined; + var lastKnownTime = undefined; + var lowerBound = undefined; + var upperBound = 0; + var maxTime = 0; + + for(var i = 1; i < this.data.rows.length; i ++) { + time = this.data.rows[i][user.index]; + + if(!time && lastKnownTime && connected) { time = lastKnownTime } + else if(time) { + if(!lowerBound) { lowerBound = i } + var point = { x: i, y: time }; + + maxTime = Math.max(maxTime, time); + lastKnownTime = time; + upperBound = i; + points.push(point); + xValues.push(point.x); + yValues.push(point.y); + } + timesToChart.push(time); + } + + userData.name = user.name; + userData.points = points; + userData.xValues = xValues; + userData.yValues = yValues; + userData.timesToChart = timesToChart; + userData.maxTime = maxTime; + userData.lowerXBound = lowerBound; + userData.upperXBound = upperBound; + + return userData; + } +} + +module.exports = GoogleSheetParser; From eba61afda0e0e5593159f5bc650e3f44eca2ab3d Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 4 Dec 2016 13:20:24 -0800 Subject: [PATCH 22/24] clean up logic for parsing all users --- .../resources/google/google_sheet_parser.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/main/autobot/resources/google/google_sheet_parser.js b/main/autobot/resources/google/google_sheet_parser.js index 8614513..c0fd423 100644 --- a/main/autobot/resources/google/google_sheet_parser.js +++ b/main/autobot/resources/google/google_sheet_parser.js @@ -4,11 +4,6 @@ var spline = require('cubic-spline'); class GoogleSheetParser { constructor(data) { this.data = data; - if(this.data.filters.length == 0) { - this.users = this.extractUsers([], true); - } else { - this.users = this.extractUsers(this.data.filters, false); - } } /** @@ -22,12 +17,10 @@ class GoogleSheetParser { parse(options) { var parsedData = { users: {} }; var totalMaxTime = 0; - var users = []; var connected = !options.interpolate - var extractAllUsers = (this.data.filters.length == 0); // if our filters length is zero then we parse all user data - users = this.extractUsers(this.data.filters, extractAllUsers); + var users = this.extractUsers(this.data.filters); var totalBound = this.data.rows.length -1; var numUsers = users.length; @@ -60,7 +53,7 @@ class GoogleSheetParser { **/ interpolateData(userData, numUsers, totalBound) { var interpolatedData = []; - var numberOfDataPoints = Math.floor(1900/numUsers); + var numberOfDataPoints = Math.floor(1700/numUsers); var incrementer = totalBound / numberOfDataPoints; for(var x = 1; x < totalBound; x+= incrementer) { @@ -81,14 +74,13 @@ class GoogleSheetParser { * This should parse out the raw data and return the array of indexes corresponding to each filter name * Input: * filters: ['not_found', 'dude_found', 'your_mom_found'] - * extractAllUsers: false * Output: * [ * { name: dude_found, index: 1 }, * { name: your_mom_found, index: 13 } * ] */ - extractUsers(filters, extractAllUsers) { + extractUsers(filters) { var userRows = this.data.rows[0]; var extractedUsers = []; @@ -96,7 +88,10 @@ class GoogleSheetParser { var name = userRows[i].toLowerCase(); var user = {}; - if(extractAllUsers || _.contains(filters, name)) { + // If we have no filters, then we extract all viable users + var extractUser = (filters.length == 0 && i > 1) || _.contains(filters, name); + + if(extractUser) { user.name = name; user.index = i; extractedUsers.push(user); From f51c3395c31647205c3f61d7a4c8a022bcd1bcd7 Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 4 Dec 2016 13:23:31 -0800 Subject: [PATCH 23/24] add jira credentials to pre-setup script for tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 33c178e..da419d4 100755 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "scripts": { "start": "node ./main/autobot/cli.js", "pretest": "rsync -av --ignore-existing ./configs/user_mappings.js.example ./configs/user_mappings.js", + "pretest": "rsync -av --ignore-existing ./configs/jira_credentials.js.example ./configs/jira_credentials.js", "test": "mocha --recursive" }, "dependencies": { From cc62248acc5aaddb0dbd8aa7b0d16ebf2337d95a Mon Sep 17 00:00:00 2001 From: abMatGit Date: Sun, 4 Dec 2016 14:01:22 -0800 Subject: [PATCH 24/24] finishing touches --- main/autobot/adapters/cli.js | 10 +++++++--- main/autobot/adapters/slack.js | 12 ++++++++---- main/autobot/resources/google/google_chart.js | 2 ++ main/autobot/resources/google/google_sheet_parser.js | 2 ++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/main/autobot/adapters/cli.js b/main/autobot/adapters/cli.js index f440b40..c391f3d 100755 --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -5,6 +5,8 @@ var Adapter = require('../adapters/adapter'); class Parser { constructor(input) { this.input = input; + this.commandIndex = 0; + this.argsIndex = 1; } parse() { @@ -28,7 +30,7 @@ class Parser { } fetchCommand() { - return this.input.trim().split(' ')[0]; + return this.input.trim().split(' ')[this.commandIndex]; } parseUpdate() { @@ -60,13 +62,15 @@ class Parser { parseInterpolate() { var regexUsers = /([a-zA-z]+)/gi - var matchedUsers = this.input.match(regexUsers).slice(1); + var matchedUsers = this.input.match(regexUsers).slice(this.argsIndex); return { command: 'interpolate', args: matchedUsers }; } parseDefault() { - return { command: this.fetchCommand(), args: this.input.split(' ').slice(1) } + var command = this.fetchCommand(); + var args = this.input.split(' ').slice(this.argsIndex); + return { command: command, args: args } } } diff --git a/main/autobot/adapters/slack.js b/main/autobot/adapters/slack.js index 8fe522b..a284294 100755 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -9,6 +9,8 @@ class Parser { } else { this.input = input; } + this.commandIndex = 1; + this.argsIndex = 2; } parse() { @@ -44,7 +46,7 @@ class Parser { } fetchCommand() { - return this.input.trim().split(' ')[1]; + return this.input.trim().split(' ')[this.commandIndex]; } parseUpdate() { @@ -69,20 +71,22 @@ class Parser { parseChart() { var regexUsers = /[a-zA-z]+/gi - var matchedUsers = this.input.match(regexUsers); + var matchedUsers = this.input.match(regexUsers).slice(this.argsIndex); return { command: 'chart', args: matchedUsers }; } parseInterpolate() { var regexUsers = /([a-zA-z]+)/gi - var matchedUsers = this.input.match(regexUsers).slice(1); + var matchedUsers = this.input.match(regexUsers).slice(this.argsIndex); return { command: 'interpolate', args: matchedUsers }; } parseDefault() { - return { command: this.fetchCommand(), args: this.input.split(' ').slice(2) } + var command = this.fetchCommand(); + var args = this.input.split(' ').slice(this.argsIndex); + return { command: command, args: args } } } diff --git a/main/autobot/resources/google/google_chart.js b/main/autobot/resources/google/google_chart.js index d3036d6..d95927d 100644 --- a/main/autobot/resources/google/google_chart.js +++ b/main/autobot/resources/google/google_chart.js @@ -10,6 +10,8 @@ } } */ +'use strict'; + class GoogleChart { constructor(data) { this.users = data.users; diff --git a/main/autobot/resources/google/google_sheet_parser.js b/main/autobot/resources/google/google_sheet_parser.js index c0fd423..2919dcc 100644 --- a/main/autobot/resources/google/google_sheet_parser.js +++ b/main/autobot/resources/google/google_sheet_parser.js @@ -1,3 +1,5 @@ +'use strict'; + var _= require('underscore'); var spline = require('cubic-spline');