diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index c3e4012..aec7e77 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ #Credentials -*/*_credentials.js +configs/*.js +configs/*.json # Logs logs diff --git a/circle.yml b/circle.yml old mode 100644 new mode 100755 diff --git a/configs/google_credentials.js.example b/configs/google_credentials.js.example new file mode 100755 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/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 index e169767..1dcacfb --- 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/configs/user_mappings.js.example b/configs/user_mappings.js.example new file mode 100755 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" +} 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 index 17f9438..6cc1088 --- a/handler.js +++ b/handler.js @@ -1,10 +1,13 @@ '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) { +module.exports.slack = function(event, context, callback) {}; + +module.exports.updateSheet = function(event, context, callback) { var statusCode, text; var body = event.body; @@ -23,12 +26,12 @@ module.exports.slack = 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'); - autobot.receive(params.text).then(success, failure); } }; diff --git a/main/autobot.js b/main/autobot.js old mode 100644 new mode 100755 index 4ad6ab2..4683d22 --- a/main/autobot.js +++ b/main/autobot.js @@ -3,11 +3,14 @@ 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) { var adapterClass = access(Adapters, adapter); - this.adapter = new adapterClass(); + if(coreType == 'google') { this.adapter = new adapterClass(googleCore); } + else { this.adapter = new adapterClass(defaultCore); } } receive(input) { 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 d137426..c391f3d --- a/main/autobot/adapters/cli.js +++ b/main/autobot/adapters/cli.js @@ -2,6 +2,78 @@ var Adapter = require('../adapters/adapter'); +class Parser { + constructor(input) { + this.input = input; + this.commandIndex = 0; + this.argsIndex = 1; + } + + parse() { + if(this.input) { + var command = this.fetchCommand(); + + switch(command) { + case 'update': + return this.parseUpdate(); + case 'chart': + return this.parseChart(); + case 'interpolate': + return this.parseInterpolate(); + default: + return this.parseDefault(); + } + } + else { + return { command: '', args: {} } + } + } + + fetchCommand() { + return this.input.trim().split(' ')[this.commandIndex]; + } + + parseUpdate() { + 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 = {}; + + 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: 'update', args: parsedArgs }; + } + + parseChart() { + var regexUsers = /[a-zA-z]+/gi + var matchedUsers = this.input.match(regexUsers); + + return { command: 'chart', args: matchedUsers }; + } + + parseInterpolate() { + var regexUsers = /([a-zA-z]+)/gi + var matchedUsers = this.input.match(regexUsers).slice(this.argsIndex); + + return { command: 'interpolate', args: matchedUsers }; + } + + parseDefault() { + var command = this.fetchCommand(); + var args = this.input.split(' ').slice(this.argsIndex); + return { command: command, args: args } + } +} + class Cli extends Adapter { /* Right now this parsing is very dumb. @@ -10,11 +82,10 @@ class Cli extends Adapter { for more intelligent mapping. */ parse(input) { - 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 index 801c606..a284294 --- a/main/autobot/adapters/slack.js +++ b/main/autobot/adapters/slack.js @@ -1,15 +1,113 @@ 'use strict'; var Adapter = require('../adapters/adapter'); +class Parser { + constructor(input) { + if(input.text) { + this.input = input.text; + this.username = input.user_name; + } else { + this.input = input; + } + this.commandIndex = 1; + this.argsIndex = 2; + } + + parse() { + if(this.fetchGreeting() != null) { + return this.fetchGreeting(); + } else if(this.input) { + var command = this.fetchCommand(); + + switch(command) { + case 'update': + return this.parseUpdate(); + case 'chart': + return this.parseChart(); + case 'interpolate': + return this.parseInterpolate(); + 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: {} } + } + + return null; + } + + fetchCommand() { + return this.input.trim().split(' ')[this.commandIndex]; + } + + parseUpdate() { + 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 = {}; + + 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: 'update', args: parsedArgs }; + } + + parseChart() { + var regexUsers = /[a-zA-z]+/gi + 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(this.argsIndex); + + return { command: 'interpolate', args: matchedUsers }; + } + + parseDefault() { + var command = this.fetchCommand(); + var args = this.input.split(' ').slice(this.argsIndex); + return { command: command, args: args } + } +} class Slack extends Adapter { parse(input) { - var tokens = input.split(' '); - return { command: tokens[1], args: tokens.slice(2) } + var parser = new Parser(input); + return parser.parse(); } render(data) { - return { text: JSON.stringify(data) }; + //responseData = JSON.stringify(data); + //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/cli.js b/main/autobot/cli.js old mode 100644 new mode 100755 index a1c4416..abfe7f2 --- 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/core/core.js b/main/autobot/core/core.js old mode 100644 new mode 100755 index ccabcc7..cee5e09 --- 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/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,32 @@ class Core { var commandToken = inputTokens['command']; var args = inputTokens['args']; - var cmd = access(this.commands, commandToken).bind(this); + if(commandToken == '') { return doNothing; } + 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); + } + } - return cmd(args); + type() { + return access(this.commands, 'type').bind(this).call(); } } -var commands = { +// 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'; }, /* A simple echo call. @@ -64,5 +86,48 @@ var commands = { } } +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); + }, + + chart: function(args) { + return this.resource.chart(args); + }, + + interpolate: function(args) { + return this.resource.interpolate(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); 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/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..d95927d --- /dev/null +++ b/main/autobot/resources/google/google_chart.js @@ -0,0 +1,143 @@ +// ************************ 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] + } + } +*/ +'use strict'; + +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..2919dcc --- /dev/null +++ b/main/autobot/resources/google/google_sheet_parser.js @@ -0,0 +1,164 @@ +'use strict'; + +var _= require('underscore'); +var spline = require('cubic-spline'); + +class GoogleSheetParser { + constructor(data) { + this.data = data; + } + + /** + * Output: + * { + * totalMaxTime: 40, + * users: { mexTaco: , pierogi: , hotSauce: }, + * totalBound: 4 + * } + **/ + parse(options) { + var parsedData = { users: {} }; + var totalMaxTime = 0; + var connected = !options.interpolate + + // if our filters length is zero then we parse all user data + var users = this.extractUsers(this.data.filters); + + 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(1700/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'] + * Output: + * [ + * { name: dude_found, index: 1 }, + * { name: your_mom_found, index: 13 } + * ] + */ + extractUsers(filters) { + var userRows = this.data.rows[0]; + var extractedUsers = []; + + for(var i = 0; i < userRows.length; i++) { + var name = userRows[i].toLowerCase(); + var user = {}; + + // 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); + } + } + + 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; 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 index 4a9833c..da419d4 --- a/package.json +++ b/package.json @@ -16,12 +16,21 @@ }, "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": { + "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", - "strict-mode": "^1.0.0" + "strict-mode": "^1.0.0", + "underscore": "^1.8.3" } } diff --git a/serverless.yml b/serverless.yml old mode 100644 new mode 100755 index f071d8c..3eba502 --- 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 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 index 4304232..4c399ae --- 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); }); }); 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