From 43ebaa71c617bf4a7abaeaec999053d14acf5c8b Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sun, 22 Mar 2020 18:48:08 -0700 Subject: [PATCH 01/39] Add initial design in comments --- index.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/index.js b/index.js index 22d61f1..f9692ca 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,46 @@ const mathjs = require('mathjs') const bot = new Bot() +// TO-DO: Implement initial availability design +// +// /avail get userName utcOffset +// Gets all availabilities in for a given user in the requested utcOffset +// userName: the Keybase username of the user whose availability is requested +// utcOffset: the UTC offset to apply to the dates requested +// Example: /avail get dorg +08:00 +// Availbilities for user dorg: +// 1. 50 default +// 2. 0 3/25/2020-3/27/2020 +// 3. 25 4/29/2021-5/1/2021 +// 4. 75 5/2/2021-5/10/2021 +// +// /avail set workLevel +// Sets the default availability for the user sending the message, +// overriding the previous default value// worklevel: number 0-100, e.g. 50 +// Example: /avail add 75 +// Added default availability of 50 for dorg +// +// /avail add workLevel utcOffset dateSignalBegin dateSignalEnd +// Adds the availability for the user sending the message +// worklevel: number 0-100, e.g. 50 +// utcOffset: the UTC offset for the input dates +// dateSignalBegin: start date of provided availability, e.g. 1/1/2020 +// dateSignalEnd: optional, end date of provided availability, e.g. 3/1/2020, +// Example: /avail add 50 +08:00 3/25/2020 3/27/2020 +// Added availability 50 +08:00 3/25/2020-3/27/2020 for dorg +// +// /avail rm utcOffset +// Removes an availability for the user sending the message +// via an interative dialogue +// utcOffset: the UTC offset to apply to the dates requested +// Example: /avail rm +// dorg, Which availibity would you like to remove? +// 1. 50 default +// 2. 0 3/25/2020-3/27/2020 +// 3. 25 4/29/2021-5/1/2021 +// 4. 75 5/2/2021-5/10/2021 +// + const msgReply = s => { let a1, a2, ans, b1, b2, eqn try { From 6844ab76d5069140b9dcea362be5d835b1dd5104 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sun, 22 Mar 2020 19:04:08 -0700 Subject: [PATCH 02/39] Fix typos --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index f9692ca..fe56afa 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,7 @@ const bot = new Bot() // userName: the Keybase username of the user whose availability is requested // utcOffset: the UTC offset to apply to the dates requested // Example: /avail get dorg +08:00 -// Availbilities for user dorg: +// Availabilities for user dorg: // 1. 50 default // 2. 0 3/25/2020-3/27/2020 // 3. 25 4/29/2021-5/1/2021 @@ -43,10 +43,10 @@ const bot = new Bot() // // /avail rm utcOffset // Removes an availability for the user sending the message -// via an interative dialogue +// via an interactive dialogue // utcOffset: the UTC offset to apply to the dates requested // Example: /avail rm -// dorg, Which availibity would you like to remove? +// dorg, Which availability would you like to remove? // 1. 50 default // 2. 0 3/25/2020-3/27/2020 // 3. 25 4/29/2021-5/1/2021 From b1f4db32b1380f32c543fc1471a54b76f763b990 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sun, 22 Mar 2020 19:12:17 -0700 Subject: [PATCH 03/39] Fix formatting of comment --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fe56afa..08e0c08 100644 --- a/index.js +++ b/index.js @@ -28,7 +28,8 @@ const bot = new Bot() // // /avail set workLevel // Sets the default availability for the user sending the message, -// overriding the previous default value// worklevel: number 0-100, e.g. 50 +// overriding the previous default value +// worklevel: number 0-100, e.g. 50 // Example: /avail add 75 // Added default availability of 50 for dorg // From ee0c06ecc7b91691e77985a6a76da404d1145d25 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:03:26 -0700 Subject: [PATCH 04/39] Move to Typescript --- .gitignore | 12 ++++++ Dockerfile | 5 --- README.md | 69 ++++++++++++++++++++++++++++++++ index.js => src/index.ts | 42 ++++++------------- package.json => src/package.json | 7 +++- src/tasks.json | 19 +++++++++ src/tsconfig.json | 59 +++++++++++++++++++++++++++ yarn.lock => src/yarn.lock | 61 ++++++---------------------- 8 files changed, 188 insertions(+), 86 deletions(-) delete mode 100644 Dockerfile rename index.js => src/index.ts (69%) rename package.json => src/package.json (57%) create mode 100644 src/tasks.json create mode 100644 src/tsconfig.json rename yarn.lock => src/yarn.lock (50%) diff --git a/.gitignore b/.gitignore index 51c5b10..d1ffb78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,15 @@ +_temp/ +.vscode +.DS_Store +gulp-tsc-tmp-* +.gulp-tsc-tmp-* +output/ +*.js +*.js.map +*.d.ts +config.json +!gulpfile.js + # Logs logs *.log diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a9cedb9..0000000 --- a/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM keybaseio/client:nightly-node -WORKDIR /app -COPY . /app -RUN yarn install -CMD node /app/index.js diff --git a/README.md b/README.md index 0d6e263..be8cff1 100644 --- a/README.md +++ b/README.md @@ -1 +1,70 @@ # AgentAvailability + +This Keybase bot is used to signal dOrg Agent availability. + +## Running locally + +You will need to run npm install in the `src` folder to get started: + +```bash +npm install +``` + +## Debugging locally + +Set up these two files in a `.vscode` folder at the root of the Git repository to debug within Visual Studio Code: + +`launch.json` - replace example values in env's nested properties as appropriate + +```json +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "configurations": [ + { + "env": { + "KB_USERNAME": "keybase_username", + "KB_PAPERKEY": "keybase_ paperkey" + }, + "name": "Launch Program", + "outFiles": [ + "${workspaceFolder}/src/output/*.js" + ], + "outputCapture": "std", + "program": "${workspaceFolder}//src//output//index.js", + "request": "launch", + "smartStep": true, + "sourceMaps": true, + "type": "node" + } + ], + "version": "3.0.1" +} +``` + +`tasks.json` + +```json +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "3.0.1", + "tasks": [ + { + "type": "typescript", + "tsconfig": "src\\tsconfig.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} +``` + +Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. \ No newline at end of file diff --git a/index.js b/src/index.ts similarity index 69% rename from index.js rename to src/index.ts index 08e0c08..48d3fc2 100644 --- a/index.js +++ b/src/index.ts @@ -1,15 +1,8 @@ #!/usr/bin/env node -const Bot = require('keybase-bot') -const mathjs = require('mathjs') +import Bot from 'keybase-bot' -// // This bot replies to any message from any user, -// starting with `/math` (in any channel) -// by actually trying to do the math. For example -// send it : -// -// /math sqrt(pi/2) * 3!` -// +// starting with `/avail` (in any channel) const bot = new Bot() @@ -54,43 +47,32 @@ const bot = new Bot() // 4. 75 5/2/2021-5/10/2021 // -const msgReply = s => { - let a1, a2, ans, b1, b2, eqn - try { - ans = '= ' + mathjs['eval'](s).toString() - } catch (e) { - a1 = Math.floor(Math.random() * 10) - b1 = Math.floor(Math.random() * 10) - a2 = Math.floor(Math.random() * 10) - b2 = Math.floor(Math.random() * 10) - eqn = '(' + a1 + ' + ' + b1 + 'i) * (' + a2 + ' + ' + b2 + 'i)' - ans = "Sorry, I can't do that math. Did you know " + eqn + ' = ' + mathjs['eval'](eqn).toString() + '? True.' - } - return ans +const msgReply = (s: any) => { + return "Hello, there"; } function main() { const username = process.env.KB_USERNAME const paperkey = process.env.KB_PAPERKEY bot - .init(username, paperkey) + .init(username || '', paperkey || '') .then(() => { - console.log('I am me!', bot.myInfo().username, bot.myInfo().devicename) + console.log('I am me!', bot.myInfo()?.username, bot.myInfo()?.devicename) console.log('Beginning watch for new messages.') - console.log(`Tell anyone to send a message to ${bot.myInfo().username} starting with '/math '`) - const onMessage = message => { + console.log(`Tell anyone to send a message to ${bot.myInfo()?.username} starting with '/avail '`) + const onMessage = (message:any) => { if (message.content.type === 'text') { - const prefix = message.content.text.body.slice(0, 6) - if (prefix === '/math ') { + const prefix = message.content.text.body.slice(0, 7) + if (prefix === '/avail ') { const reply = {body: msgReply(message.content.text.body.slice(6))} bot.chat.send(message.conversationId, reply) } } } - const onError = e => console.error(e) + const onError = (e:any) => console.error(e) bot.chat.watchAllChannelsForNewMessages(onMessage, onError) }) - .catch(error => { + .catch((error:any) => { console.error(error) shutDown() }) diff --git a/package.json b/src/package.json similarity index 57% rename from package.json rename to src/package.json index bb1b9b7..dfa183b 100644 --- a/package.json +++ b/src/package.json @@ -6,7 +6,10 @@ "license": "MIT", "private": true, "dependencies": { - "keybase-bot": "^3.6.1", - "mathjs": "^6.6.1" + "@types/mathjs": "^6.0.4", + "keybase-bot": "^3.6.1" + }, + "devDependencies": { + "@types/node": "^13.9.5" } } diff --git a/src/tasks.json b/src/tasks.json new file mode 100644 index 0000000..a327420 --- /dev/null +++ b/src/tasks.json @@ -0,0 +1,19 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "3.0.1", + "tasks": [ + { + "type": "typescript", + "tsconfig": "src\\tsconfig.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..07d007e --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [ "es2015" ], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./output", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} \ No newline at end of file diff --git a/yarn.lock b/src/yarn.lock similarity index 50% rename from yarn.lock rename to src/yarn.lock index f2b6926..2241e5a 100644 --- a/yarn.lock +++ b/src/yarn.lock @@ -2,36 +2,28 @@ # yarn lockfile v1 -complex.js@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.11.tgz#09a873fbf15ffd8c18c9c2201ccef425c32b8bf1" - integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw== +"@types/mathjs@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/mathjs/-/mathjs-6.0.4.tgz#0adf7335b27a5385e0dd4f766e0c53931c5c2375" + integrity sha512-hIxf6lfCuUbsI/iz5cevHQjKvSS+XIGPwUyYZ4GjUPrUCh9egUhLlK0d7V31jtSt1WGt6dlclnlYMNOov9JZAA== + dependencies: + decimal.js "^10.0.0" + +"@types/node@^13.9.5": + version "13.9.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00" + integrity sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw== -decimal.js@^10.2.0: +decimal.js@^10.0.0: version "10.2.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== -escape-latex@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" - integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== - -fraction.js@^4.0.12: - version "4.0.12" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.12.tgz#0526d47c65a5fb4854df78bc77f7bec708d7b8c3" - integrity sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA== - isexe@2.0.0, isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -javascript-natural-sort@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" - integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k= - keybase-bot@^3.6.1: version "3.6.1" resolved "https://registry.yarnpkg.com/keybase-bot/-/keybase-bot-3.6.1.tgz#d0262ed8359026926da00b69b43c53104fcfd0bf" @@ -59,20 +51,6 @@ lodash.snakecase@4.1.1: resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40= -mathjs@^6.6.1: - version "6.6.1" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-6.6.1.tgz#46675c9e97b8cb8cf9a66402b1360a6996abf103" - integrity sha512-RCFCYkf1IV3u0DAeqj2Rqqwyi302kFxHoYbfp/Bxm6kUg0ALYH7YT0bYzsO8qgCLv9RS3bWMZnAgUbLgiDjLcw== - dependencies: - complex.js "^2.0.11" - decimal.js "^10.2.0" - escape-latex "^1.2.0" - fraction.js "^4.0.12" - javascript-natural-sort "^0.7.1" - seed-random "^2.2.0" - tiny-emitter "^2.1.0" - typed-function "^1.1.1" - minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -85,21 +63,6 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" -seed-random@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54" - integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ= - -tiny-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - -typed-function@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-1.1.1.tgz#a1316187ec3628c9e219b91ca96918660a10138e" - integrity sha512-RbN7MaTQBZLJYzDENHPA0nUmWT0Ex80KHItprrgbTPufYhIlTePvCXZxyQK7wgn19FW5bnuaBIKcBb5mRWjB1Q== - which@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From becdc6022914c9c183be66f7ba790fd6e838bbcd Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:07:55 -0700 Subject: [PATCH 05/39] Fix readme typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index be8cff1..1e8cd7b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This Keybase bot is used to signal dOrg Agent availability. You will need to run npm install in the `src` folder to get started: ```bash -npm install +yarn install ``` ## Debugging locally @@ -25,7 +25,7 @@ Set up these two files in a `.vscode` folder at the root of the Git repository t { "env": { "KB_USERNAME": "keybase_username", - "KB_PAPERKEY": "keybase_ paperkey" + "KB_PAPERKEY": "keybase_paperkey" }, "name": "Launch Program", "outFiles": [ From 137dea8515f9d90c07e3ed147839e0343aee788b Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:09:09 -0700 Subject: [PATCH 06/39] Fix additional readme typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e8cd7b..349c96c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This Keybase bot is used to signal dOrg Agent availability. ## Running locally -You will need to run npm install in the `src` folder to get started: +You will need to run yarn install in the `src` folder to get started: ```bash yarn install From 57d990a8d38837b098a34f329786c842c7521dae Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:25:39 -0700 Subject: [PATCH 07/39] Add example usage to readme --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 349c96c..de425d9 100644 --- a/README.md +++ b/README.md @@ -67,4 +67,44 @@ Set up these two files in a `.vscode` folder at the root of the Git repository t } ``` -Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. \ No newline at end of file +Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. + +## Example usage + +```bash +User: /avail get usera +Bot: usera has not set their availability +``` +```bash +User: /avail get userb +Bot: Availability for user userb: +Default: 50% +Time Zone: EST (-05:00) +- [3/25/2020 - 3/27/2020] 0% +- [3/28/2020 - 4/28/2020] 50% +- [4/29/2020 - 5/01/2020] 25% +- [5/02/2020 - 5/10/2020] 75% +``` +```bash +User: /avail get userb +01:00 +Bot: Availability for user userb: +Default: 50% +Time Zone: CET (+01:00) +... uses specified time zone "CET" instead of userb's time zone of "EST" +``` +```bash +User: /avail config set default 80% +Bot: Your default availability has been set to 80% +``` +```bash +User: /avail add 0% 7/10/2020 7/30/2020 +Bot: Please set your time-zone first +``` +```bash +User: /avail config set time-zone CET +Bot: Your time-zone has been updated to CET (+01:00) +``` +User: /avail add 0% 7/10/2020 7/30/2020 +```bash +Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) +``` \ No newline at end of file From 568e4480e45702900082c97f85ef65a6f0e59a58 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:31:07 -0700 Subject: [PATCH 08/39] Fix formatting of examples in readme --- README.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index de425d9..efaa402 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,12 @@ Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger ## Example usage -```bash +``` User: /avail get usera Bot: usera has not set their availability ``` -```bash + +``` User: /avail get userb Bot: Availability for user userb: Default: 50% @@ -85,26 +86,31 @@ Time Zone: EST (-05:00) - [4/29/2020 - 5/01/2020] 25% - [5/02/2020 - 5/10/2020] 75% ``` -```bash + +``` User: /avail get userb +01:00 Bot: Availability for user userb: Default: 50% Time Zone: CET (+01:00) -... uses specified time zone "CET" instead of userb's time zone of "EST" ``` -```bash +Uses specified time zone "CET" instead of userb's time zone of "EST" + +``` User: /avail config set default 80% Bot: Your default availability has been set to 80% ``` -```bash + +``` User: /avail add 0% 7/10/2020 7/30/2020 Bot: Please set your time-zone first ``` -```bash + +``` User: /avail config set time-zone CET Bot: Your time-zone has been updated to CET (+01:00) +``` + ``` User: /avail add 0% 7/10/2020 7/30/2020 -```bash Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) ``` \ No newline at end of file From cca4f7592ec44f15fe82f0b590e7fd4b04c9c107 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:33:02 -0700 Subject: [PATCH 09/39] Move examples to top of readme --- README.md | 94 +++++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index efaa402..d6ef660 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,52 @@ This Keybase bot is used to signal dOrg Agent availability. +## Example usage + +``` +User: /avail get usera +Bot: usera has not set their availability +``` + +``` +User: /avail get userb +Bot: Availability for user userb: +Default: 50% +Time Zone: EST (-05:00) +- [3/25/2020 - 3/27/2020] 0% +- [3/28/2020 - 4/28/2020] 50% +- [4/29/2020 - 5/01/2020] 25% +- [5/02/2020 - 5/10/2020] 75% +``` + +``` +User: /avail get userb +01:00 +Bot: Availability for user userb: +Default: 50% +Time Zone: CET (+01:00) +``` +Uses specified time zone "CET" instead of userb's time zone of "EST" + +``` +User: /avail config set default 80% +Bot: Your default availability has been set to 80% +``` + +``` +User: /avail add 0% 7/10/2020 7/30/2020 +Bot: Please set your time-zone first +``` + +``` +User: /avail config set time-zone CET +Bot: Your time-zone has been updated to CET (+01:00) +``` + +``` +User: /avail add 0% 7/10/2020 7/30/2020 +Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) +``` + ## Running locally You will need to run yarn install in the `src` folder to get started: @@ -67,50 +113,4 @@ Set up these two files in a `.vscode` folder at the root of the Git repository t } ``` -Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. - -## Example usage - -``` -User: /avail get usera -Bot: usera has not set their availability -``` - -``` -User: /avail get userb -Bot: Availability for user userb: -Default: 50% -Time Zone: EST (-05:00) -- [3/25/2020 - 3/27/2020] 0% -- [3/28/2020 - 4/28/2020] 50% -- [4/29/2020 - 5/01/2020] 25% -- [5/02/2020 - 5/10/2020] 75% -``` - -``` -User: /avail get userb +01:00 -Bot: Availability for user userb: -Default: 50% -Time Zone: CET (+01:00) -``` -Uses specified time zone "CET" instead of userb's time zone of "EST" - -``` -User: /avail config set default 80% -Bot: Your default availability has been set to 80% -``` - -``` -User: /avail add 0% 7/10/2020 7/30/2020 -Bot: Please set your time-zone first -``` - -``` -User: /avail config set time-zone CET -Bot: Your time-zone has been updated to CET (+01:00) -``` - -``` -User: /avail add 0% 7/10/2020 7/30/2020 -Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) -``` \ No newline at end of file +Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. \ No newline at end of file From 48cae76f212bfda843a0db505a2f01d4b4b21856 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 12:58:44 -0700 Subject: [PATCH 10/39] Slight refactor, added additional examples to readme --- README.md | 32 +++++++++++++++++++++++++++---- src/index.ts | 54 +++++----------------------------------------------- 2 files changed, 33 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d6ef660..9b5721b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,17 @@ This Keybase bot is used to signal dOrg Agent availability. ## Example usage +``` +User: /avail get +Bot: Availability for user usera: +Default: 50% +Time Zone: EST (-05:00) +- [3/25/2020 - 3/27/2020] 0% +- [3/28/2020 - 4/28/2020] 50% +- [4/29/2020 - 5/01/2020] 25% +- [5/02/2020 - 5/10/2020] 75% +``` + ``` User: /avail get usera Bot: usera has not set their availability @@ -29,18 +40,18 @@ Time Zone: CET (+01:00) Uses specified time zone "CET" instead of userb's time zone of "EST" ``` -User: /avail config set default 80% +User: /avail set default 80% Bot: Your default availability has been set to 80% ``` ``` User: /avail add 0% 7/10/2020 7/30/2020 -Bot: Please set your time-zone first +Bot: Please set your time zone first ``` ``` -User: /avail config set time-zone CET -Bot: Your time-zone has been updated to CET (+01:00) +User: /avail set timezone CET +Bot: Your time zone has been updated to CET (+01:00) ``` ``` @@ -48,6 +59,19 @@ User: /avail add 0% 7/10/2020 7/30/2020 Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) ``` +``` +User: /avail rm +Bot: Which availability would you like to remove? +Default: 50% +Time Zone: EST (-05:00) +1. [3/25/2020 - 3/27/2020] 0% +2. [3/28/2020 - 4/28/2020] 50% +3. [4/29/2020 - 5/01/2020] 25% +4. [5/02/2020 - 5/10/2020] 75% +User: 1 +Bot: Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00) +``` + ## Running locally You will need to run yarn install in the `src` folder to get started: diff --git a/src/index.ts b/src/index.ts index 48d3fc2..d25ed99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,51 +1,8 @@ #!/usr/bin/env node import Bot from 'keybase-bot' -// This bot replies to any message from any user, -// starting with `/avail` (in any channel) - const bot = new Bot() - -// TO-DO: Implement initial availability design -// -// /avail get userName utcOffset -// Gets all availabilities in for a given user in the requested utcOffset -// userName: the Keybase username of the user whose availability is requested -// utcOffset: the UTC offset to apply to the dates requested -// Example: /avail get dorg +08:00 -// Availabilities for user dorg: -// 1. 50 default -// 2. 0 3/25/2020-3/27/2020 -// 3. 25 4/29/2021-5/1/2021 -// 4. 75 5/2/2021-5/10/2021 -// -// /avail set workLevel -// Sets the default availability for the user sending the message, -// overriding the previous default value -// worklevel: number 0-100, e.g. 50 -// Example: /avail add 75 -// Added default availability of 50 for dorg -// -// /avail add workLevel utcOffset dateSignalBegin dateSignalEnd -// Adds the availability for the user sending the message -// worklevel: number 0-100, e.g. 50 -// utcOffset: the UTC offset for the input dates -// dateSignalBegin: start date of provided availability, e.g. 1/1/2020 -// dateSignalEnd: optional, end date of provided availability, e.g. 3/1/2020, -// Example: /avail add 50 +08:00 3/25/2020 3/27/2020 -// Added availability 50 +08:00 3/25/2020-3/27/2020 for dorg -// -// /avail rm utcOffset -// Removes an availability for the user sending the message -// via an interactive dialogue -// utcOffset: the UTC offset to apply to the dates requested -// Example: /avail rm -// dorg, Which availability would you like to remove? -// 1. 50 default -// 2. 0 3/25/2020-3/27/2020 -// 3. 25 4/29/2021-5/1/2021 -// 4. 75 5/2/2021-5/10/2021 -// +const commandPrefix:string = '/avail ' const msgReply = (s: any) => { return "Hello, there"; @@ -57,13 +14,12 @@ function main() { bot .init(username || '', paperkey || '') .then(() => { - console.log('I am me!', bot.myInfo()?.username, bot.myInfo()?.devicename) - console.log('Beginning watch for new messages.') - console.log(`Tell anyone to send a message to ${bot.myInfo()?.username} starting with '/avail '`) + console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename) + console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`) const onMessage = (message:any) => { if (message.content.type === 'text') { - const prefix = message.content.text.body.slice(0, 7) - if (prefix === '/avail ') { + const prefix = message.content.text.body.slice(0, commandPrefix.length) + if (prefix === commandPrefix) { const reply = {body: msgReply(message.content.text.body.slice(6))} bot.chat.send(message.conversationId, reply) } From 7d95f681b3a710407f839a4b7d10a8324776f3a4 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 13:51:08 -0700 Subject: [PATCH 11/39] Added scaffolding for functionality --- src/index.ts | 84 +++++++++++++++++++++++++++++++++++++----------- src/package.json | 15 --------- 2 files changed, 65 insertions(+), 34 deletions(-) delete mode 100644 src/package.json diff --git a/src/index.ts b/src/index.ts index d25ed99..3330c67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,44 +1,90 @@ #!/usr/bin/env node import Bot from 'keybase-bot' +import { MsgSummary } from 'keybase-bot/lib/types/chat1' const bot = new Bot() const commandPrefix:string = '/avail ' +const commandVerbs = { + addVerb: 'add', + getVerb: 'get', + setVerb: 'set', + rmVerb: 'rm', +} + +const addValue = (args:string[]) => { + return 'Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00)'; +} + +const getValues = (args:string[]) => { + return `Availability for user ...: + Default: 50% + Time Zone: EST (-05:00) + - [3/25/2020 - 3/27/2020] 0% + - [3/28/2020 - 4/28/2020] 50% + - [4/29/2020 - 5/01/2020] 25% + - [5/02/2020 - 5/10/2020] 75%`; +} + +const setValue = (args:string[]) => { + return 'setValue'; +} + +const rmValue = (args:string[]) => { + return 'rmValue'; +} -const msgReply = (s: any) => { - return "Hello, there"; +const msgReply = (message: MsgSummary) => { + let args:string[] = message?.content?.text?.body.split(" ") || []; + if (args[1] === commandVerbs.addVerb) { + return addValue(args.splice(0, 2)); + } + else if (args[1] === commandVerbs.getVerb) { + return getValues(args.splice(0, 2)); + } + else if(args[1] === commandVerbs.setVerb) { + return setValue(args.splice(0, 2)); + } + else if(args[1] === commandVerbs.rmVerb) { + return rmValue(args.splice(0, 2)); + } + else { + let errorMessage:string = `Invalid command verb: ${args[2]}`; + console.error(errorMessage); + return errorMessage; + } } function main() { - const username = process.env.KB_USERNAME - const paperkey = process.env.KB_PAPERKEY + const username = process.env.KB_USERNAME; + const paperkey = process.env.KB_PAPERKEY; bot .init(username || '', paperkey || '') .then(() => { - console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename) - console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`) - const onMessage = (message:any) => { - if (message.content.type === 'text') { - const prefix = message.content.text.body.slice(0, commandPrefix.length) + console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename); + console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`); + const onMessage = (message:MsgSummary) => { + if (message?.content.type === 'text') { + const prefix = message?.content?.text?.body.slice(0, commandPrefix.length); if (prefix === commandPrefix) { - const reply = {body: msgReply(message.content.text.body.slice(6))} - bot.chat.send(message.conversationId, reply) + const reply = {body: msgReply(message)}; + bot.chat.send(message.conversationId, reply); } } } - const onError = (e:any) => console.error(e) - bot.chat.watchAllChannelsForNewMessages(onMessage, onError) + const onError = (e:any) => console.error(e); + bot.chat.watchAllChannelsForNewMessages(onMessage, onError); }) .catch((error:any) => { - console.error(error) - shutDown() + console.error(error); + shutDown(); }) } function shutDown() { - bot.deinit().then(() => process.exit()) + bot.deinit().then(() => process.exit()); } -process.on('SIGINT', shutDown) -process.on('SIGTERM', shutDown) +process.on('SIGINT', shutDown); +process.on('SIGTERM', shutDown); -main() +main(); diff --git a/src/package.json b/src/package.json deleted file mode 100644 index dfa183b..0000000 --- a/src/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "AgentAvailability", - "version": "1.0.0", - "main": "index.js", - "author": "", - "license": "MIT", - "private": true, - "dependencies": { - "@types/mathjs": "^6.0.4", - "keybase-bot": "^3.6.1" - }, - "devDependencies": { - "@types/node": "^13.9.5" - } -} From 55b56ee1d78b0f4594b5dca4a1ecfcc71f2a8e18 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 14:37:52 -0700 Subject: [PATCH 12/39] Added additional scaffolding for functionality --- README.md | 3 +- src/index.ts | 112 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9b5721b..bbb8405 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ Time Zone: EST (-05:00) 2. [3/28/2020 - 4/28/2020] 50% 3. [4/29/2020 - 5/01/2020] 25% 4. [5/02/2020 - 5/10/2020] 75% -User: 1 +Respond with /avail rm # +User: /avail rm 1 Bot: Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00) ``` diff --git a/src/index.ts b/src/index.ts index 3330c67..494e3ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,50 +2,120 @@ import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' -const bot = new Bot() -const commandPrefix:string = '/avail ' +// TO-DO: Implement storage +const bot = new Bot(); +const commandPrefix:string = '/avail '; const commandVerbs = { addVerb: 'add', getVerb: 'get', setVerb: 'set', rmVerb: 'rm', -} +}; +const configKeys = { + default: 'default', + timezone: 'timezone' +}; -const addValue = (args:string[]) => { - return 'Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00)'; +// TO-DO: Add validation that timezone has been set before adding +// TO-DO: Add validation of arguments +const addValue = (args:string[], username:string) => { + if (args[0] && args[1]) { + if (args[3]) { + return `Added availability of ${args[0]} for ${args[1]} ${args[2]} timezone`; + } + else { + return `Added availability of ${args[0]} for ${args[1]} ${args[1]} timezone`; + } + } + else { + let errorMessage:string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } } -const getValues = (args:string[]) => { - return `Availability for user ...: - Default: 50% - Time Zone: EST (-05:00) - - [3/25/2020 - 3/27/2020] 0% - - [3/28/2020 - 4/28/2020] 50% - - [4/29/2020 - 5/01/2020] 25% - - [5/02/2020 - 5/10/2020] 75%`; +// TO-DO: Add validation of username at args[0] +const getValues = (args:string[], username:string) => { + if (!args[0]) { + return `Availability for user ${username}: + Default: 50% + Time Zone: EST (-05:00) + - [3/25/2020 - 3/27/2020] 0% + - [3/28/2020 - 4/28/2020] 50% + - [4/29/2020 - 5/01/2020] 25% + - [5/02/2020 - 5/10/2020] 75%`; + } + else if (args[0] && args[0] !== '') { + return `Availability for user ${args[0]}: + Default: 50% + Time Zone: EST (-05:00) + - [3/25/2020 - 3/27/2020] 0% + - [3/28/2020 - 4/28/2020] 50% + - [4/29/2020 - 5/01/2020] 25% + - [5/02/2020 - 5/10/2020] 75%`; + } + else { + let errorMessage:string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } } -const setValue = (args:string[]) => { - return 'setValue'; +// TO-DO: Validate work level at args[1] +// TO-DO: Validate timezone at args[1] +const setValue = (args:string[], username:string) => { + if (args[0] === configKeys.default) { + return `Your default availability has been set to ${args[1]}`; + } + else if(args[0] === configKeys.timezone) { + return `Your time zone has been updated to ${args[1]}`; + } + else { + let errorMessage:string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } } -const rmValue = (args:string[]) => { - return 'rmValue'; +// TO-DO: Verify args[0] is a key of an availability +const rmValue = (args:string[], username:string) => { + if (args.length === 0) { + return `Which availability would you like to remove? + Default: 50% + Time Zone: EST (-05:00) + 1. [3/25/2020 - 3/27/2020] 0% + 2. [3/28/2020 - 4/28/2020] 50% + 3. [4/29/2020 - 5/01/2020] 25% + 4. [5/02/2020 - 5/10/2020] 75% + Respond with /avail rm #`; + } + else if (args[0] && !isNaN(Number(args[0]))) { + return 'Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00)' + } + else { + let errorMessage:string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } } const msgReply = (message: MsgSummary) => { let args:string[] = message?.content?.text?.body.split(" ") || []; if (args[1] === commandVerbs.addVerb) { - return addValue(args.splice(0, 2)); + args.splice(0, 2); + return addValue(args, message?.sender?.username || ''); } else if (args[1] === commandVerbs.getVerb) { - return getValues(args.splice(0, 2)); + args.splice(0, 2); + return getValues(args, message?.sender?.username || ''); } else if(args[1] === commandVerbs.setVerb) { - return setValue(args.splice(0, 2)); + args.splice(0, 2); + return setValue(args, message?.sender?.username || ''); } else if(args[1] === commandVerbs.rmVerb) { - return rmValue(args.splice(0, 2)); + args.splice(0, 2); + return rmValue(args, message?.sender?.username || ''); } else { let errorMessage:string = `Invalid command verb: ${args[2]}`; From e64926e069e31a530b4082432cbdfc01993c69e1 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 15:23:15 -0700 Subject: [PATCH 13/39] Implement scaffolding for validation --- src/index.ts | 87 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index 494e3ef..2e9eef8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,9 @@ import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' -// TO-DO: Implement storage +// TO-DO: Implement storage and CRUD operations in functions const bot = new Bot(); -const commandPrefix:string = '/avail '; +const commandPrefix: string = '/avail '; const commandVerbs = { addVerb: 'add', getVerb: 'get', @@ -16,26 +16,47 @@ const configKeys = { timezone: 'timezone' }; +// TO-DO: Implement validation +const isValidDate = (date: string): boolean => { + return true; +} +const isValidTimezone = (date: string): boolean => { + return true; +} +const isValidUsername = (date: string): boolean => { + return true; +} +const isValidWorkLevel = (date: string): boolean => { + return true; +} + +const writeArgsErrorMessage = (args: string[]): string => { + let errorMessage: string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; +} + // TO-DO: Add validation that timezone has been set before adding -// TO-DO: Add validation of arguments -const addValue = (args:string[], username:string) => { - if (args[0] && args[1]) { - if (args[3]) { +const addValue = (args: string[], username: string): string => { + if (isValidWorkLevel(args[0]) && + isValidDate(args[1])) { + if (isValidDate(args[2])) { return `Added availability of ${args[0]} for ${args[1]} ${args[2]} timezone`; } + else if (!isValidDate(args[2])) { + return writeArgsErrorMessage(args); + } else { return `Added availability of ${args[0]} for ${args[1]} ${args[1]} timezone`; } } else { - let errorMessage:string = `Invalid arguments: ${args.toString()}`; - console.error(errorMessage); - return errorMessage; + return writeArgsErrorMessage(args); } } // TO-DO: Add validation of username at args[0] -const getValues = (args:string[], username:string) => { +const getValues = (args: string[], username: string) => { if (!args[0]) { return `Availability for user ${username}: Default: 50% @@ -45,7 +66,7 @@ const getValues = (args:string[], username:string) => { - [4/29/2020 - 5/01/2020] 25% - [5/02/2020 - 5/10/2020] 75%`; } - else if (args[0] && args[0] !== '') { + else if (isValidUsername(args[0])) { return `Availability for user ${args[0]}: Default: 50% Time Zone: EST (-05:00) @@ -55,30 +76,26 @@ const getValues = (args:string[], username:string) => { - [5/02/2020 - 5/10/2020] 75%`; } else { - let errorMessage:string = `Invalid arguments: ${args.toString()}`; - console.error(errorMessage); - return errorMessage; + return writeArgsErrorMessage(args); } } -// TO-DO: Validate work level at args[1] -// TO-DO: Validate timezone at args[1] -const setValue = (args:string[], username:string) => { - if (args[0] === configKeys.default) { +const setValue = (args: string[], username: string) => { + if (args[0] === configKeys.default && + isValidWorkLevel(args[1])) { return `Your default availability has been set to ${args[1]}`; } - else if(args[0] === configKeys.timezone) { + else if (args[0] === configKeys.timezone && + isValidTimezone(args[1])) { return `Your time zone has been updated to ${args[1]}`; } else { - let errorMessage:string = `Invalid arguments: ${args.toString()}`; - console.error(errorMessage); - return errorMessage; + return writeArgsErrorMessage(args); } } // TO-DO: Verify args[0] is a key of an availability -const rmValue = (args:string[], username:string) => { +const rmValue = (args: string[], username: string) => { if (args.length === 0) { return `Which availability would you like to remove? Default: 50% @@ -93,14 +110,12 @@ const rmValue = (args:string[], username:string) => { return 'Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00)' } else { - let errorMessage:string = `Invalid arguments: ${args.toString()}`; - console.error(errorMessage); - return errorMessage; + return writeArgsErrorMessage(args); } } -const msgReply = (message: MsgSummary) => { - let args:string[] = message?.content?.text?.body.split(" ") || []; +const msgReply = (message: MsgSummary): string => { + let args: string[] = message?.content?.text?.body.split(" ") || []; if (args[1] === commandVerbs.addVerb) { args.splice(0, 2); return addValue(args, message?.sender?.username || ''); @@ -109,17 +124,17 @@ const msgReply = (message: MsgSummary) => { args.splice(0, 2); return getValues(args, message?.sender?.username || ''); } - else if(args[1] === commandVerbs.setVerb) { + else if (args[1] === commandVerbs.setVerb) { args.splice(0, 2); return setValue(args, message?.sender?.username || ''); } - else if(args[1] === commandVerbs.rmVerb) { + else if (args[1] === commandVerbs.rmVerb) { args.splice(0, 2); return rmValue(args, message?.sender?.username || ''); } else { - let errorMessage:string = `Invalid command verb: ${args[2]}`; - console.error(errorMessage); + let errorMessage: string = `Invalid command verb: ${args[2]}`; + console.error(errorMessage); return errorMessage; } } @@ -132,19 +147,19 @@ function main() { .then(() => { console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename); console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`); - const onMessage = (message:MsgSummary) => { + const onMessage = (message: MsgSummary) => { if (message?.content.type === 'text') { const prefix = message?.content?.text?.body.slice(0, commandPrefix.length); if (prefix === commandPrefix) { - const reply = {body: msgReply(message)}; + const reply = { body: msgReply(message) }; bot.chat.send(message.conversationId, reply); } } } - const onError = (e:any) => console.error(e); + const onError = (e: any) => console.error(e); bot.chat.watchAllChannelsForNewMessages(onMessage, onError); }) - .catch((error:any) => { + .catch((error: any) => { console.error(error); shutDown(); }) From ac1e1f331ec2cbf8e22c90a2d77b5fea8134d642 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 28 Mar 2020 16:05:21 -0700 Subject: [PATCH 14/39] Added initial percentage validation logic --- src/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2e9eef8..1f7561e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,14 +20,21 @@ const configKeys = { const isValidDate = (date: string): boolean => { return true; } -const isValidTimezone = (date: string): boolean => { +const isValidTimezone = (timezone: string): boolean => { return true; } -const isValidUsername = (date: string): boolean => { +const isValidUsername = (username: string): boolean => { return true; } -const isValidWorkLevel = (date: string): boolean => { - return true; +const isValidWorkLevel = (worklevel: string): boolean => { + // TO-DO: Fix percentage validation logic + if (worklevel && + worklevel[worklevel.length - 1] === '%') { //&& + //worklevel.slice(worklevel.length - 2).split('').every(char => char >= '0' && char <= '9')) { //&& + //Number(worklevel.slice(worklevel.length - 2)) <= 100) { + return true; + } + return false; } const writeArgsErrorMessage = (args: string[]): string => { From 41d0d8e9dafa349ba24d80a74e20e5ca7e3f61da Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 4 Apr 2020 13:38:30 -0700 Subject: [PATCH 15/39] Added initial CRUD operations for Keybase keyvalue store --- src/index.ts | 120 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 33 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1f7561e..8395ba6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' -// TO-DO: Implement storage and CRUD operations in functions const bot = new Bot(); const commandPrefix: string = '/avail '; const commandVerbs = { @@ -15,6 +14,12 @@ const configKeys = { default: 'default', timezone: 'timezone' }; +const nameSpaces = { + availabilities: 'AgentAvailability.Availabilities', + defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', + timezones: 'AgentAvailability.TimeZones', +} +const teamName = 'test040420'; // TO-DO: Implement validation const isValidDate = (date: string): boolean => { @@ -43,18 +48,32 @@ const writeArgsErrorMessage = (args: string[]): string => { return errorMessage; } -// TO-DO: Add validation that timezone has been set before adding -const addValue = (args: string[], username: string): string => { +const timezoneNotSetErrormessage = (username: string): string => { + let errorMessage: string = `Timezone has not been set for user ${username}`; + console.error(errorMessage); + return errorMessage; +} + +// TO-DO: Add support for multiple availabilities +// TO-DO: Add conversion of dates to user's timezone +async function addValue (args: string[], username: string): Promise { + let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue + if (timezone === '') { + return timezoneNotSetErrormessage(username); + } + if (isValidWorkLevel(args[0]) && isValidDate(args[1])) { if (isValidDate(args[2])) { - return `Added availability of ${args[0]} for ${args[1]} ${args[2]} timezone`; + await bot.kvstore.put(teamName, nameSpaces.availabilities, username, `${args[0]} ${args[1]} ${args[2]}`); + return `Added availability of ${args[0]} for ${args[1]} ${args[2]} ${timezone}`; } else if (!isValidDate(args[2])) { return writeArgsErrorMessage(args); } else { - return `Added availability of ${args[0]} for ${args[1]} ${args[1]} timezone`; + await bot.kvstore.put(teamName, nameSpaces.availabilities, username, `${args[0]} ${args[1]} ${args[1]}`); + return `Added availability of ${args[0]} for ${args[1]} ${args[1]} ${timezone}`; } } else { @@ -63,37 +82,58 @@ const addValue = (args: string[], username: string): string => { } // TO-DO: Add validation of username at args[0] -const getValues = (args: string[], username: string) => { +// TO-DO: Add support for multiple availabilities +// TO-DO: Add conversion of dates to user's timezone +// TO-DO: Add conversion of dates to a specified timezone +async function getValues (args: string[], username: string) : Promise { if (!args[0]) { + let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; + let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; + if (timezone === '') { + return timezoneNotSetErrormessage(username); + } + if (availability === '') { + return `No availabilities set for user ${username}: + Default: ${defaultWorkLevel} + Time Zone: ${timezone}` + } return `Availability for user ${username}: - Default: 50% - Time Zone: EST (-05:00) - - [3/25/2020 - 3/27/2020] 0% - - [3/28/2020 - 4/28/2020] 50% - - [4/29/2020 - 5/01/2020] 25% - - [5/02/2020 - 5/10/2020] 75%`; + Default: ${defaultWorkLevel} + Time Zone: ${timezone} + - ${availability}`; } else if (isValidUsername(args[0])) { + let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, args[0])).entryValue; + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, args[0])).entryValue; + let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, args[0])).entryValue; + if (timezone === '') { + return timezoneNotSetErrormessage(args[0]); + } + if (availability === '') { + return `No availabilities set for user ${args[0]}: + Default: ${defaultWorkLevel} + Time Zone: ${timezone}` + } return `Availability for user ${args[0]}: - Default: 50% - Time Zone: EST (-05:00) - - [3/25/2020 - 3/27/2020] 0% - - [3/28/2020 - 4/28/2020] 50% - - [4/29/2020 - 5/01/2020] 25% - - [5/02/2020 - 5/10/2020] 75%`; + Default: ${defaultWorkLevel} + Time Zone: ${timezone} + - ${availability}`; } else { return writeArgsErrorMessage(args); } } -const setValue = (args: string[], username: string) => { +async function setValue (args: string[], username: string): Promise { if (args[0] === configKeys.default && isValidWorkLevel(args[1])) { + await bot.kvstore.put(teamName, nameSpaces.defaultWorkLevels, username, args[1]); return `Your default availability has been set to ${args[1]}`; } else if (args[0] === configKeys.timezone && isValidTimezone(args[1])) { + await bot.kvstore.put(teamName, nameSpaces.timezones, username, args[1]); return `Your time zone has been updated to ${args[1]}`; } else { @@ -101,27 +141,34 @@ const setValue = (args: string[], username: string) => { } } -// TO-DO: Verify args[0] is a key of an availability -const rmValue = (args: string[], username: string) => { +// TO-DO: Add support for multiple availabilities +// TO-DO: Add conversion of dates to user's timezone +async function rmValue (args: string[], username: string): Promise { + let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; + if (timezone === '') { + return timezoneNotSetErrormessage(username); + } + if (args.length === 0) { + let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? - Default: 50% - Time Zone: EST (-05:00) - 1. [3/25/2020 - 3/27/2020] 0% - 2. [3/28/2020 - 4/28/2020] 50% - 3. [4/29/2020 - 5/01/2020] 25% - 4. [5/02/2020 - 5/10/2020] 75% + Default: ${defaultWorkLevel} + Time Zone: ${timezone} + 1. ${availability} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { - return 'Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00)' + // TO-DO: Verify args[0] is a key of an availability + await bot.kvstore.delete(teamName, nameSpaces.availabilities, username); + return `Removed availability of 0% for 3/25/2020 3/27/2020 ${timezone}`; } else { return writeArgsErrorMessage(args); } } -const msgReply = (message: MsgSummary): string => { +async function msgReply (message: MsgSummary) : Promise { let args: string[] = message?.content?.text?.body.split(" ") || []; if (args[1] === commandVerbs.addVerb) { args.splice(0, 2); @@ -146,7 +193,7 @@ const msgReply = (message: MsgSummary): string => { } } -function main() { +async function main() { const username = process.env.KB_USERNAME; const paperkey = process.env.KB_PAPERKEY; bot @@ -154,11 +201,11 @@ function main() { .then(() => { console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename); console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`); - const onMessage = (message: MsgSummary) => { + async function onMessage (message: MsgSummary): Promise { if (message?.content.type === 'text') { const prefix = message?.content?.text?.body.slice(0, commandPrefix.length); if (prefix === commandPrefix) { - const reply = { body: msgReply(message) }; + const reply = { body: await msgReply(message) }; bot.chat.send(message.conversationId, reply); } } @@ -179,4 +226,11 @@ function shutDown() { process.on('SIGINT', shutDown); process.on('SIGTERM', shutDown); -main(); +(async () => { + try { + var text = await main(); + console.log(text); + } catch (e) { + console.log(e) + } +})(); From 5990e5315f453d8302fe4d4424302d6a58d63d08 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 4 Apr 2020 14:45:54 -0700 Subject: [PATCH 16/39] Add support for CRUD operations on multiple availabilities --- src/index.ts | 150 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 88 insertions(+), 62 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8395ba6..f77a3fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,16 @@ const nameSpaces = { } const teamName = 'test040420'; +// TO-DO: Return formatted string +const getAvailabilitiesString = (availabilities: object[]): string => { + return JSON.stringify(availabilities); +} + +// TO-DO: Return formatted string +const getAvailabilityString = (availability: object): string => { + return JSON.stringify(availability); +} + // TO-DO: Implement validation const isValidDate = (date: string): boolean => { return true; @@ -54,78 +64,79 @@ const timezoneNotSetErrormessage = (username: string): string => { return errorMessage; } -// TO-DO: Add support for multiple availabilities // TO-DO: Add conversion of dates to user's timezone -async function addValue (args: string[], username: string): Promise { +async function addValue(args: string[], username: string): Promise { + let newAvailability = { + startDate: '', + endDate: '', + workLevel: '' + } let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue if (timezone === '') { return timezoneNotSetErrormessage(username); } - if (isValidWorkLevel(args[0]) && - isValidDate(args[1])) { - if (isValidDate(args[2])) { - await bot.kvstore.put(teamName, nameSpaces.availabilities, username, `${args[0]} ${args[1]} ${args[2]}`); - return `Added availability of ${args[0]} for ${args[1]} ${args[2]} ${timezone}`; - } - else if (!isValidDate(args[2])) { - return writeArgsErrorMessage(args); - } - else { - await bot.kvstore.put(teamName, nameSpaces.availabilities, username, `${args[0]} ${args[1]} ${args[1]}`); - return `Added availability of ${args[0]} for ${args[1]} ${args[1]} ${timezone}`; - } + if (!isValidWorkLevel(args[0]) && + !isValidDate(args[1])) { + return writeArgsErrorMessage(args); } - else { + if (args[2] && + !isValidDate(args[2])) { return writeArgsErrorMessage(args); } + + newAvailability.workLevel = args[0]; + newAvailability.startDate = args[1]; + if (!args[2]) { + newAvailability.endDate = args[1]; + } + else { + newAvailability.endDate = args[2]; + } + + let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let availabilities: object[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } + availabilities.push(newAvailability); + await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); + return `Added availability of ${getAvailabilityString(newAvailability)} ${timezone}`; } -// TO-DO: Add validation of username at args[0] -// TO-DO: Add support for multiple availabilities // TO-DO: Add conversion of dates to user's timezone // TO-DO: Add conversion of dates to a specified timezone -async function getValues (args: string[], username: string) : Promise { - if (!args[0]) { - let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; - let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; - if (timezone === '') { - return timezoneNotSetErrormessage(username); +async function getValues(args: string[], username: string): Promise { + if (args[0]) { + if (isValidUsername(args[0])) { + username = args[0] } - if (availability === '') { - return `No availabilities set for user ${username}: + else { + return writeArgsErrorMessage(args); + } + } + + let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; + let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; + if (timezone === '') { + return timezoneNotSetErrormessage(username); + } + if (availabilitiesString === '') { + return `No availabilities set for user ${username}: Default: ${defaultWorkLevel} Time Zone: ${timezone}` - } - return `Availability for user ${username}: - Default: ${defaultWorkLevel} - Time Zone: ${timezone} - - ${availability}`; - } - else if (isValidUsername(args[0])) { - let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, args[0])).entryValue; - let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, args[0])).entryValue; - let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, args[0])).entryValue; - if (timezone === '') { - return timezoneNotSetErrormessage(args[0]); - } - if (availability === '') { - return `No availabilities set for user ${args[0]}: - Default: ${defaultWorkLevel} - Time Zone: ${timezone}` - } - return `Availability for user ${args[0]}: + } + + let availabilities: object[] = JSON.parse(availabilitiesString); + return `Availability for user ${username}: Default: ${defaultWorkLevel} Time Zone: ${timezone} - - ${availability}`; - } - else { - return writeArgsErrorMessage(args); - } + ${getAvailabilitiesString(availabilities)}`; + } -async function setValue (args: string[], username: string): Promise { +async function setValue(args: string[], username: string): Promise { if (args[0] === configKeys.default && isValidWorkLevel(args[1])) { await bot.kvstore.put(teamName, nameSpaces.defaultWorkLevels, username, args[1]); @@ -141,26 +152,41 @@ async function setValue (args: string[], username: string): Promise { } } -// TO-DO: Add support for multiple availabilities // TO-DO: Add conversion of dates to user's timezone -async function rmValue (args: string[], username: string): Promise { +async function rmValue(args: string[], username: string): Promise { let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; if (timezone === '') { return timezoneNotSetErrormessage(username); } if (args.length === 0) { - let availability = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let availabilities: object[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} Time Zone: ${timezone} - 1. ${availability} + ${getAvailabilitiesString(availabilities)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { - // TO-DO: Verify args[0] is a key of an availability - await bot.kvstore.delete(teamName, nameSpaces.availabilities, username); + let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; + let availabilities: object[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } + + if (availabilities[Number(args[0])]) { + availabilities.splice(Number(args[0]), 1); + } + else { + return writeArgsErrorMessage(args); + } + + await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); return `Removed availability of 0% for 3/25/2020 3/27/2020 ${timezone}`; } else { @@ -168,7 +194,7 @@ async function rmValue (args: string[], username: string): Promise { } } -async function msgReply (message: MsgSummary) : Promise { +async function msgReply(message: MsgSummary): Promise { let args: string[] = message?.content?.text?.body.split(" ") || []; if (args[1] === commandVerbs.addVerb) { args.splice(0, 2); @@ -201,7 +227,7 @@ async function main() { .then(() => { console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename); console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`); - async function onMessage (message: MsgSummary): Promise { + async function onMessage(message: MsgSummary): Promise { if (message?.content.type === 'text') { const prefix = message?.content?.text?.body.slice(0, commandPrefix.length); if (prefix === commandPrefix) { @@ -228,8 +254,8 @@ process.on('SIGTERM', shutDown); (async () => { try { - var text = await main(); - console.log(text); + var text = await main(); + console.log(text); } catch (e) { console.log(e) } From 9b930d10bc885b80c1678d2ba86e68d100a33d85 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 4 Apr 2020 15:35:48 -0700 Subject: [PATCH 17/39] Improve output formatting --- README.md | 3 ++- src/index.ts | 57 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index bbb8405..4ba4b17 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ Set up these two files in a `.vscode` folder at the root of the Git repository t { "env": { "KB_USERNAME": "keybase_username", - "KB_PAPERKEY": "keybase_paperkey" + "KB_PAPERKEY": "keybase_paperkey", + "KB_TEAMNAME": "keybase_teamname" }, "name": "Launch Program", "outFiles": [ diff --git a/src/index.ts b/src/index.ts index f77a3fd..b7ded37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,16 +19,26 @@ const nameSpaces = { defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', timezones: 'AgentAvailability.TimeZones', } -const teamName = 'test040420'; +const paperkey = process.env.KB_PAPERKEY; +const teamName = process.env.KB_TEAMNAME; +const username = process.env.KB_USERNAME; -// TO-DO: Return formatted string -const getAvailabilitiesString = (availabilities: object[]): string => { - return JSON.stringify(availabilities); +type Availability = { + startDate: string, + endDate: string, + workLevel: string } -// TO-DO: Return formatted string -const getAvailabilityString = (availability: object): string => { - return JSON.stringify(availability); +const getAvailabilitiesString = (availabilities: Availability[]): string => { + let availabilitiesString = ''; + availabilities.forEach((item, index) => { + availabilitiesString += `\r\n${index+1}. [${item.startDate} - ${item.endDate}] ${item.workLevel}`; + }); + return availabilitiesString; +} + +const getAvailabilityString = (availability: Availability): string => { + return `[${availability.startDate} - ${availability.endDate}] ${availability.workLevel}`; } // TO-DO: Implement validation @@ -66,7 +76,7 @@ const timezoneNotSetErrormessage = (username: string): string => { // TO-DO: Add conversion of dates to user's timezone async function addValue(args: string[], username: string): Promise { - let newAvailability = { + let newAvailability: Availability = { startDate: '', endDate: '', workLevel: '' @@ -123,16 +133,17 @@ async function getValues(args: string[], username: string): Promise { return timezoneNotSetErrormessage(username); } if (availabilitiesString === '') { - return `No availabilities set for user ${username}: - Default: ${defaultWorkLevel} - Time Zone: ${timezone}` + return `${username} has not set their availability` } - let availabilities: object[] = JSON.parse(availabilitiesString); + let availabilities: Availability[] = JSON.parse(availabilitiesString); + + if (availabilities.length === 0) { + return `${username} has not set their availability` + } return `Availability for user ${username}: Default: ${defaultWorkLevel} - Time Zone: ${timezone} - ${getAvailabilitiesString(availabilities)}`; + Time Zone: ${timezone} ${getAvailabilitiesString(availabilities)}`; } @@ -161,15 +172,21 @@ async function rmValue(args: string[], username: string): Promise { if (args.length === 0) { let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let availabilities: object[] = []; + let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); } + else { + return `${username} has not set their availability` + } + if (availabilities.length === 0) { + return `${username} has not set their availability` + } + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} - Time Zone: ${timezone} - ${getAvailabilitiesString(availabilities)} + Time Zone: ${timezone} ${getAvailabilitiesString(availabilities)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { @@ -179,8 +196,8 @@ async function rmValue(args: string[], username: string): Promise { availabilities = JSON.parse(availabilitiesString); } - if (availabilities[Number(args[0])]) { - availabilities.splice(Number(args[0]), 1); + if (availabilities[Number(args[0])-1]) { + availabilities.splice(Number(args[0])-1, 1); } else { return writeArgsErrorMessage(args); @@ -220,8 +237,6 @@ async function msgReply(message: MsgSummary): Promise { } async function main() { - const username = process.env.KB_USERNAME; - const paperkey = process.env.KB_PAPERKEY; bot .init(username || '', paperkey || '') .then(() => { From 8c1f63e9b4ac876934303a2b655ab2b4670bf8b8 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 11 Apr 2020 12:08:01 -0700 Subject: [PATCH 18/39] Add date validation --- src/index.ts | 7 ++++++- src/package.json | 7 +++++++ src/yarn.lock | 25 +++++++++---------------- 3 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 src/package.json diff --git a/src/index.ts b/src/index.ts index b7ded37..c22ba0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' +import moment, { Moment } from 'moment' const bot = new Bot(); const commandPrefix: string = '/avail '; @@ -43,7 +44,11 @@ const getAvailabilityString = (availability: Availability): string => { // TO-DO: Implement validation const isValidDate = (date: string): boolean => { - return true; + let validatedDate:Moment = moment(date, 'M/DD/YYYY', true); + if (validatedDate.isValid()) { + return true; + } + return false; } const isValidTimezone = (timezone: string): boolean => { return true; diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..e5a3436 --- /dev/null +++ b/src/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "@types/node": "^13.11.1", + "keybase-bot": "^3.6.1", + "moment": "^2.24.0" + } +} diff --git a/src/yarn.lock b/src/yarn.lock index 2241e5a..fe18c25 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -2,22 +2,10 @@ # yarn lockfile v1 -"@types/mathjs@^6.0.4": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@types/mathjs/-/mathjs-6.0.4.tgz#0adf7335b27a5385e0dd4f766e0c53931c5c2375" - integrity sha512-hIxf6lfCuUbsI/iz5cevHQjKvSS+XIGPwUyYZ4GjUPrUCh9egUhLlK0d7V31jtSt1WGt6dlclnlYMNOov9JZAA== - dependencies: - decimal.js "^10.0.0" - -"@types/node@^13.9.5": - version "13.9.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00" - integrity sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw== - -decimal.js@^10.0.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231" - integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw== +"@types/node@^13.11.1": + version "13.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" + integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== isexe@2.0.0, isexe@^2.0.0: version "2.0.0" @@ -63,6 +51,11 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" +moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + which@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From a1ed73847b384f10c49d6cd23ccc904a91897521 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 11 Apr 2020 14:12:23 -0700 Subject: [PATCH 19/39] Add work level validation regex --- src/index.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index c22ba0d..f3639f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,8 @@ const nameSpaces = { const paperkey = process.env.KB_PAPERKEY; const teamName = process.env.KB_TEAMNAME; const username = process.env.KB_USERNAME; +// Regex for a valid integer 0-100 followed by a % +const workLevelRegex = /^(?:100|[1-9]?[0-9])%{1}$/ type Availability = { startDate: string, @@ -33,7 +35,7 @@ type Availability = { const getAvailabilitiesString = (availabilities: Availability[]): string => { let availabilitiesString = ''; availabilities.forEach((item, index) => { - availabilitiesString += `\r\n${index+1}. [${item.startDate} - ${item.endDate}] ${item.workLevel}`; + availabilitiesString += `\r\n${index + 1}. [${item.startDate} - ${item.endDate}] ${item.workLevel}`; }); return availabilitiesString; } @@ -44,25 +46,24 @@ const getAvailabilityString = (availability: Availability): string => { // TO-DO: Implement validation const isValidDate = (date: string): boolean => { - let validatedDate:Moment = moment(date, 'M/DD/YYYY', true); + let validatedDate: Moment = moment(date, 'M/DD/YYYY', true); if (validatedDate.isValid()) { return true; } return false; } + const isValidTimezone = (timezone: string): boolean => { return true; } + const isValidUsername = (username: string): boolean => { return true; } + const isValidWorkLevel = (worklevel: string): boolean => { - // TO-DO: Fix percentage validation logic - if (worklevel && - worklevel[worklevel.length - 1] === '%') { //&& - //worklevel.slice(worklevel.length - 2).split('').every(char => char >= '0' && char <= '9')) { //&& - //Number(worklevel.slice(worklevel.length - 2)) <= 100) { - return true; + if (workLevelRegex.test(worklevel)) { + return true } return false; } @@ -187,7 +188,7 @@ async function rmValue(args: string[], username: string): Promise { if (availabilities.length === 0) { return `${username} has not set their availability` } - + let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} @@ -201,8 +202,8 @@ async function rmValue(args: string[], username: string): Promise { availabilities = JSON.parse(availabilitiesString); } - if (availabilities[Number(args[0])-1]) { - availabilities.splice(Number(args[0])-1, 1); + if (availabilities[Number(args[0]) - 1]) { + availabilities.splice(Number(args[0]) - 1, 1); } else { return writeArgsErrorMessage(args); From d75d42a6bd6c2660baadfa08a85ef44bb1ceaf7d Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 11 Apr 2020 15:16:48 -0700 Subject: [PATCH 20/39] Added time zone validation --- README.md | 53 ++++++++++++++++++++++++++++++++++++++---------- src/index.ts | 9 ++++++-- src/package.json | 5 ++++- src/yarn.lock | 21 ++++++++++++++++++- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4ba4b17..1699b1c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # AgentAvailability -This Keybase bot is used to signal dOrg Agent availability. +This Keybase bot is used to signal dOrg Agent availability. +Timezones must a valid Moment timezone name. +See examples in the zones properties [here](https://github.com/moment/moment-timezone/blob/develop/data/meta/latest.json). ## Example usage @@ -8,7 +10,7 @@ This Keybase bot is used to signal dOrg Agent availability. User: /avail get Bot: Availability for user usera: Default: 50% -Time Zone: EST (-05:00) +Time Zone: America/New_York - [3/25/2020 - 3/27/2020] 0% - [3/28/2020 - 4/28/2020] 50% - [4/29/2020 - 5/01/2020] 25% @@ -24,7 +26,7 @@ Bot: usera has not set their availability User: /avail get userb Bot: Availability for user userb: Default: 50% -Time Zone: EST (-05:00) +Time Zone: America/New_York - [3/25/2020 - 3/27/2020] 0% - [3/28/2020 - 4/28/2020] 50% - [4/29/2020 - 5/01/2020] 25% @@ -32,12 +34,16 @@ Time Zone: EST (-05:00) ``` ``` -User: /avail get userb +01:00 +User: /avail get userb America/Los_Angeles Bot: Availability for user userb: Default: 50% -Time Zone: CET (+01:00) +Time Zone: America/Los_Angeles +- [3/25/2020 - 3/27/2020] 0% +- [3/28/2020 - 4/28/2020] 50% +- [4/29/2020 - 5/01/2020] 25% +- [5/02/2020 - 5/10/2020] 75% ``` -Uses specified time zone "CET" instead of userb's time zone of "EST" +Uses specified time zone "America/Los_Angeles" instead of userb's time zone of "America/New_York" ``` User: /avail set default 80% @@ -50,28 +56,53 @@ Bot: Please set your time zone first ``` ``` -User: /avail set timezone CET -Bot: Your time zone has been updated to CET (+01:00) +User: /avail set timezone America/New_York +Bot: Your time zone has been updated to America/New_York ``` ``` User: /avail add 0% 7/10/2020 7/30/2020 -Bot: Added availability of 0% for 7/10/2020 7/30/2020 CET (+01:00) +Bot: Added availability of 0% for 7/10/2020 7/30/2020 America/Los_Angeles ``` ``` User: /avail rm Bot: Which availability would you like to remove? Default: 50% -Time Zone: EST (-05:00) +Time Zone: America/New_York 1. [3/25/2020 - 3/27/2020] 0% 2. [3/28/2020 - 4/28/2020] 50% 3. [4/29/2020 - 5/01/2020] 25% 4. [5/02/2020 - 5/10/2020] 75% Respond with /avail rm # User: /avail rm 1 -Bot: Removed availability of 0% for 3/25/2020 3/27/2020 EST (-05:00) +Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York +``` + +## Development tasks to do +* Add better validation fail messages +* Add logging +* Refactor +* Deploy +* (Optional) Add a help verb to display docs for different commands, example: +``` +User: /avail help +Bot: Usage: /avail [verb] [parameter1] [parameter2] [parameter3] +Verbs: +get +set +rm +add +``` +``` +User: /avail help add +Bot: Usage: /avail add [workLevel%] [MM/DD/YYYY] [MM/DD/YYYY] +Examples: /avail add 0% 7/10/2020 7/30/2020 +/avail add 50% 7/10/2020 ``` +* (Optional) Add CI/CD +* (Optional) Figure out a simple way to validate keybase usernames: +May need to add the [Go client](https://github.com/keybase/client) to project or implement own [user endpoint call.](https://keybase.io/docs/api/1.0/call/user/lookup) ## Running locally diff --git a/src/index.ts b/src/index.ts index f3639f2..b9ec9ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' import moment, { Moment } from 'moment' +import momentTimezone from 'moment-timezone' const bot = new Bot(); const commandPrefix: string = '/avail '; @@ -15,6 +16,7 @@ const configKeys = { default: 'default', timezone: 'timezone' }; +const momentTimezoneNames = momentTimezone.tz.names(); const nameSpaces = { availabilities: 'AgentAvailability.Availabilities', defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', @@ -44,7 +46,6 @@ const getAvailabilityString = (availability: Availability): string => { return `[${availability.startDate} - ${availability.endDate}] ${availability.workLevel}`; } -// TO-DO: Implement validation const isValidDate = (date: string): boolean => { let validatedDate: Moment = moment(date, 'M/DD/YYYY', true); if (validatedDate.isValid()) { @@ -54,7 +55,10 @@ const isValidDate = (date: string): boolean => { } const isValidTimezone = (timezone: string): boolean => { - return true; + if (momentTimezoneNames.indexOf(timezone) > -1) { + return true; + } + return false; } const isValidUsername = (username: string): boolean => { @@ -81,6 +85,7 @@ const timezoneNotSetErrormessage = (username: string): string => { } // TO-DO: Add conversion of dates to user's timezone +// TO-DO: Add conversion of dates to UTC timezone async function addValue(args: string[], username: string): Promise { let newAvailability: Availability = { startDate: '', diff --git a/src/package.json b/src/package.json index e5a3436..5c93aa1 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,10 @@ { "dependencies": { + "@types/moment-timezone": "^0.5.13", "@types/node": "^13.11.1", + "add": "^2.0.6", "keybase-bot": "^3.6.1", - "moment": "^2.24.0" + "moment": "^2.24.0", + "moment-timezone": "^0.5.28" } } diff --git a/src/yarn.lock b/src/yarn.lock index fe18c25..d6e7837 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -2,11 +2,23 @@ # yarn lockfile v1 +"@types/moment-timezone@^0.5.13": + version "0.5.13" + resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.13.tgz#0317ccc91eb4c7f4901704166166395c39276528" + integrity sha512-SWk1qM8DRssS5YR9L4eEX7WUhK/wc96aIr4nMa6p0kTk9YhGGOJjECVhIdPEj13fvJw72Xun69gScXSZ/UmcPg== + dependencies: + moment ">=2.14.0" + "@types/node@^13.11.1": version "13.11.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== +add@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" + integrity sha1-JI8Kn25aUo7yKV2+7DBTITCuIjU= + isexe@2.0.0, isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -51,7 +63,14 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" -moment@^2.24.0: +moment-timezone@^0.5.28: + version "0.5.28" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" + integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@>=2.14.0, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== From 8b5d0376ad749af74cedc30b95d4968b3d37b7ff Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 11 Apr 2020 16:27:15 -0700 Subject: [PATCH 21/39] Add initial timezone conversion --- README.md | 28 +++++++++++++++------------- src/index.ts | 43 +++++++++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 1699b1c..5fe01d5 100644 --- a/README.md +++ b/README.md @@ -33,18 +33,6 @@ Time Zone: America/New_York - [5/02/2020 - 5/10/2020] 75% ``` -``` -User: /avail get userb America/Los_Angeles -Bot: Availability for user userb: -Default: 50% -Time Zone: America/Los_Angeles -- [3/25/2020 - 3/27/2020] 0% -- [3/28/2020 - 4/28/2020] 50% -- [4/29/2020 - 5/01/2020] 25% -- [5/02/2020 - 5/10/2020] 75% -``` -Uses specified time zone "America/Los_Angeles" instead of userb's time zone of "America/New_York" - ``` User: /avail set default 80% Bot: Your default availability has been set to 80% @@ -80,10 +68,24 @@ Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` ## Development tasks to do +* Fix bug where dates are adjusted improperly: +local->UTC->local isn't working properly * Add better validation fail messages -* Add logging +* Add logging & better error handling * Refactor * Deploy +* Add conversion to a specified time zone, example: +``` +User: /avail get userb America/Los_Angeles +Bot: Availability for user userb: +Default: 50% +Time Zone: America/Los_Angeles +- [3/25/2020 - 3/27/2020] 0% +- [3/28/2020 - 4/28/2020] 50% +- [4/29/2020 - 5/01/2020] 25% +- [5/02/2020 - 5/10/2020] 75% +``` +Uses specified time zone "America/Los_Angeles" instead of userb's time zone of "America/New_York" * (Optional) Add a help verb to display docs for different commands, example: ``` User: /avail help diff --git a/src/index.ts b/src/index.ts index b9ec9ec..dfea03c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ const configKeys = { default: 'default', timezone: 'timezone' }; +const dateFormat = 'M/D/YYYY'; const momentTimezoneNames = momentTimezone.tz.names(); const nameSpaces = { availabilities: 'AgentAvailability.Availabilities', @@ -34,20 +35,27 @@ type Availability = { workLevel: string } -const getAvailabilitiesString = (availabilities: Availability[]): string => { +const getAvailabilitiesString = (availabilities: Availability[], timezone: string): string => { let availabilitiesString = ''; availabilities.forEach((item, index) => { - availabilitiesString += `\r\n${index + 1}. [${item.startDate} - ${item.endDate}] ${item.workLevel}`; + let availability: Availability = { + startDate: item.startDate, + endDate: item.endDate, + workLevel: item.workLevel + } + availabilitiesString += `\r\n${index + 1}. ${getAvailabilityString(availability, timezone)}`; }); return availabilitiesString; } -const getAvailabilityString = (availability: Availability): string => { - return `[${availability.startDate} - ${availability.endDate}] ${availability.workLevel}`; +const getAvailabilityString = (availability: Availability, timezone: string): string => { + let startDate = momentTimezone.utc(availability.startDate, dateFormat).tz(timezone).format(dateFormat); + let endDate = momentTimezone.utc(availability.endDate, dateFormat).tz(timezone).format(dateFormat); + return `[${startDate} - ${endDate}] ${availability.workLevel}`; } const isValidDate = (date: string): boolean => { - let validatedDate: Moment = moment(date, 'M/DD/YYYY', true); + let validatedDate: Moment = moment(date, dateFormat, true); if (validatedDate.isValid()) { return true; } @@ -84,8 +92,6 @@ const timezoneNotSetErrormessage = (username: string): string => { return errorMessage; } -// TO-DO: Add conversion of dates to user's timezone -// TO-DO: Add conversion of dates to UTC timezone async function addValue(args: string[], username: string): Promise { let newAvailability: Availability = { startDate: '', @@ -120,12 +126,13 @@ async function addValue(args: string[], username: string): Promise { if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); } + newAvailability.startDate = momentTimezone(newAvailability.startDate, dateFormat).tz(timezone, true).utc().format(dateFormat); + newAvailability.endDate = momentTimezone(newAvailability.endDate, dateFormat).tz(timezone, true).utc().format(dateFormat); availabilities.push(newAvailability); await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); - return `Added availability of ${getAvailabilityString(newAvailability)} ${timezone}`; + return `Added availability of ${getAvailabilityString(newAvailability, timezone)} ${timezone}`; } -// TO-DO: Add conversion of dates to user's timezone // TO-DO: Add conversion of dates to a specified timezone async function getValues(args: string[], username: string): Promise { if (args[0]) { @@ -154,7 +161,7 @@ async function getValues(args: string[], username: string): Promise { } return `Availability for user ${username}: Default: ${defaultWorkLevel} - Time Zone: ${timezone} ${getAvailabilitiesString(availabilities)}`; + Time Zone: ${timezone} ${getAvailabilitiesString(availabilities, timezone)}`; } @@ -174,7 +181,6 @@ async function setValue(args: string[], username: string): Promise { } } -// TO-DO: Add conversion of dates to user's timezone async function rmValue(args: string[], username: string): Promise { let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; if (timezone === '') { @@ -197,17 +203,18 @@ async function rmValue(args: string[], username: string): Promise { let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} - Time Zone: ${timezone} ${getAvailabilitiesString(availabilities)} + Time Zone: ${timezone} ${getAvailabilitiesString(availabilities, timezone)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let availabilities: object[] = []; + let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); } - if (availabilities[Number(args[0]) - 1]) { + let availabilityToRemove = availabilities[Number(args[0]) - 1]; + if (availabilityToRemove) { availabilities.splice(Number(args[0]) - 1, 1); } else { @@ -215,11 +222,11 @@ async function rmValue(args: string[], username: string): Promise { } await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); - return `Removed availability of 0% for 3/25/2020 3/27/2020 ${timezone}`; - } - else { - return writeArgsErrorMessage(args); + if (availabilityToRemove) { + return `Removed availability ${getAvailabilityString(availabilityToRemove, timezone)}`; + } } + return writeArgsErrorMessage(args); } async function msgReply(message: MsgSummary): Promise { From 913feb96adec41130f20ac4780c6f4d6529f8db8 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 18 Apr 2020 13:54:31 -0700 Subject: [PATCH 22/39] Fix bug where dates' timezones are adjusted improperly --- README.md | 3 +-- src/index.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5fe01d5..80da332 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ This Keybase bot is used to signal dOrg Agent availability. Timezones must a valid Moment timezone name. See examples in the zones properties [here](https://github.com/moment/moment-timezone/blob/develop/data/meta/latest.json). +Noon in the user's local is used as the assumed time of the provided availability date to make timezone conversions more consistent. ## Example usage @@ -68,8 +69,6 @@ Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` ## Development tasks to do -* Fix bug where dates are adjusted improperly: -local->UTC->local isn't working properly * Add better validation fail messages * Add logging & better error handling * Refactor diff --git a/src/index.ts b/src/index.ts index dfea03c..681e99e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { MsgSummary } from 'keybase-bot/lib/types/chat1' import moment, { Moment } from 'moment' import momentTimezone from 'moment-timezone' +const assumedTime = '12:00'; const bot = new Bot(); const commandPrefix: string = '/avail '; const commandVerbs = { @@ -17,6 +18,7 @@ const configKeys = { timezone: 'timezone' }; const dateFormat = 'M/D/YYYY'; +const inputDateFormat = 'M/D/YYYY HH:mm'; const momentTimezoneNames = momentTimezone.tz.names(); const nameSpaces = { availabilities: 'AgentAvailability.Availabilities', @@ -49,8 +51,8 @@ const getAvailabilitiesString = (availabilities: Availability[], timezone: strin } const getAvailabilityString = (availability: Availability, timezone: string): string => { - let startDate = momentTimezone.utc(availability.startDate, dateFormat).tz(timezone).format(dateFormat); - let endDate = momentTimezone.utc(availability.endDate, dateFormat).tz(timezone).format(dateFormat); + let startDate = momentTimezone(availability.startDate).tz(timezone).format(dateFormat); + let endDate = momentTimezone(availability.endDate).tz(timezone).format(dateFormat); return `[${startDate} - ${endDate}] ${availability.workLevel}`; } @@ -126,14 +128,13 @@ async function addValue(args: string[], username: string): Promise { if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); } - newAvailability.startDate = momentTimezone(newAvailability.startDate, dateFormat).tz(timezone, true).utc().format(dateFormat); - newAvailability.endDate = momentTimezone(newAvailability.endDate, dateFormat).tz(timezone, true).utc().format(dateFormat); + newAvailability.startDate = momentTimezone(newAvailability.startDate + " " + assumedTime, inputDateFormat, timezone).format(); + newAvailability.endDate = momentTimezone(newAvailability.endDate + " " + assumedTime, inputDateFormat, timezone).format(); availabilities.push(newAvailability); await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); return `Added availability of ${getAvailabilityString(newAvailability, timezone)} ${timezone}`; } -// TO-DO: Add conversion of dates to a specified timezone async function getValues(args: string[], username: string): Promise { if (args[0]) { if (isValidUsername(args[0])) { From 910f066661089dc7eb78b18c62b1281a237573ba Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 18 Apr 2020 14:10:48 -0700 Subject: [PATCH 23/39] Added validation of moment dates --- src/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 681e99e..02a895d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -128,8 +128,16 @@ async function addValue(args: string[], username: string): Promise { if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); } - newAvailability.startDate = momentTimezone(newAvailability.startDate + " " + assumedTime, inputDateFormat, timezone).format(); - newAvailability.endDate = momentTimezone(newAvailability.endDate + " " + assumedTime, inputDateFormat, timezone).format(); + let startDate = momentTimezone(newAvailability.startDate + " " + assumedTime, inputDateFormat, timezone); + if (!startDate.isValid()) { + return `Invalid date: ${newAvailability.startDate}`; + } + let endDate = momentTimezone(newAvailability.endDate + " " + assumedTime, inputDateFormat, timezone); + if (!endDate.isValid()) { + return `Invalid date: ${newAvailability.endDate}`; + } + newAvailability.startDate = startDate.format(); + newAvailability.endDate = endDate.format(); availabilities.push(newAvailability); await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); return `Added availability of ${getAvailabilityString(newAvailability, timezone)} ${timezone}`; From ee02255119a68aa5b5feb38b54cb2107c23fd74b Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 18 Apr 2020 15:37:35 -0700 Subject: [PATCH 24/39] Initial work-in-progress refactor --- README.md | 3 - src/agentAvailabilityBot.ts | 298 ++++++++++++++++++++++++++++++++++++ src/index.ts | 294 +---------------------------------- 3 files changed, 303 insertions(+), 292 deletions(-) create mode 100644 src/agentAvailabilityBot.ts diff --git a/README.md b/README.md index 80da332..5aa9277 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,6 @@ Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` ## Development tasks to do -* Add better validation fail messages -* Add logging & better error handling -* Refactor * Deploy * Add conversion to a specified time zone, example: ``` diff --git a/src/agentAvailabilityBot.ts b/src/agentAvailabilityBot.ts new file mode 100644 index 0000000..00fb3a3 --- /dev/null +++ b/src/agentAvailabilityBot.ts @@ -0,0 +1,298 @@ +import Bot from 'keybase-bot' +import moment, { Moment } from 'moment' +import momentTimezone from 'moment-timezone' +import { MsgSummary } from 'keybase-bot/lib/types/chat1' + +type Availability = { + startDate: string, + endDate: string, + workLevel: string +} + +export class AgentAvailabilityBot { + private assumedTime: string = '12:00'; + private bot: Bot = new Bot(); + private commandPrefix: string = '/avail '; + private commandVerbs: any = { + addVerb: 'add', + getVerb: 'get', + setVerb: 'set', + rmVerb: 'rm', + } + private configKeys: any = { + default: 'default', + timezone: 'timezone' + }; + private dateFormat: string = 'M/D/YYYY'; + private inputDateFormat: string = 'M/D/YYYY HH:mm'; + private momentTimezoneNames: string[] = momentTimezone.tz.names(); + private nameSpaces: any = { + availabilities: 'AgentAvailability.Availabilities', + defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', + timezones: 'AgentAvailability.TimeZones', + } + private paperkey: string | undefined = process.env.KB_PAPERKEY; + private teamName: string | undefined = process.env.KB_TEAMNAME; + private username: string | undefined = process.env.KB_USERNAME; + // Regex for a valid integer 0-100 followed by a % + private workLevelRegex: RegExp = /^(?:100|[1-9]?[0-9])%{1}$/ + + constructor() { + + } + + public init() { + this.bot + .init(this.username || '', this.paperkey || '') + .then(this.startUp) + .catch((error: any) => { + console.error(error); + }) + .then(this.deinit); + } + + public deinit() { + this.bot.deinit().then(() => process.exit()); + } + + private startUp() { + console.log('Starting up', this.bot.myInfo()?.username, this.bot.myInfo()?.devicename); + console.log(`Watching for new messages to ${this.bot.myInfo()?.username} starting with ${this.commandPrefix}`); + const onError = (e: any) => console.error(e); + this.bot.chat.watchAllChannelsForNewMessages(this.onMessage, onError); + } + private async onMessage(message: MsgSummary): Promise { + if (message?.content.type === 'text') { + const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); + if (prefix === this.commandPrefix) { + const reply = { body: await this.msgReply(message) }; + this.bot.chat.send(message.conversationId, reply); + } + } + } + + private getAvailabilitiesString(availabilities: Availability[], timezone: string): string { + let availabilitiesString = ''; + availabilities.forEach((item, index) => { + let availability: Availability = { + startDate: item.startDate, + endDate: item.endDate, + workLevel: item.workLevel + } + availabilitiesString += `\r\n${index + 1}. ${this.getAvailabilityString(availability, timezone)}`; + }); + return availabilitiesString; + } + + private getAvailabilityString(availability: Availability, timezone: string): string { + let startDate = momentTimezone(availability.startDate).tz(timezone).format(this.dateFormat); + let endDate = momentTimezone(availability.endDate).tz(timezone).format(this.dateFormat); + return `[${startDate} - ${endDate}] ${availability.workLevel}`; + } + + private isValidDate(date: string): boolean { + let validatedDate: Moment = moment(date, this.dateFormat, true); + if (validatedDate.isValid()) { + return true; + } + return false; + } + + private isValidTimezone(timezone: string): boolean { + if (this.momentTimezoneNames.indexOf(timezone) > -1) { + return true; + } + return false; + } + + private isValidUsername(username: string): boolean { + return true; + } + + private isValidWorkLevel(worklevel: string): boolean { + if (this.workLevelRegex.test(worklevel)) { + return true + } + return false; + } + + private writeArgsErrorMessage(args: string[]): string { + let errorMessage: string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } + + private timezoneNotSetErrormessage(username: string): string { + let errorMessage: string = `Timezone has not been set for user ${username}`; + console.error(errorMessage); + return errorMessage; + } + + private async addValue(args: string[], username: string): Promise { + let newAvailability: Availability = { + startDate: '', + endDate: '', + workLevel: '' + } + let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue + if (timezone === '') { + return this.timezoneNotSetErrormessage(username); + } + + if (!this.isValidWorkLevel(args[0]) && + !this.isValidDate(args[1])) { + return this.writeArgsErrorMessage(args); + } + if (args[2] && + !this.isValidDate(args[2])) { + return this.writeArgsErrorMessage(args); + } + + newAvailability.workLevel = args[0]; + newAvailability.startDate = args[1]; + if (!args[2]) { + newAvailability.endDate = args[1]; + } + else { + newAvailability.endDate = args[2]; + } + + let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilities: object[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } + let startDate = momentTimezone(newAvailability.startDate + " " + this.assumedTime, this.inputDateFormat, timezone); + if (!startDate.isValid()) { + return `Invalid date: ${newAvailability.startDate}`; + } + let endDate = momentTimezone(newAvailability.endDate + " " + this.assumedTime, this.inputDateFormat, timezone); + if (!endDate.isValid()) { + return `Invalid date: ${newAvailability.endDate}`; + } + newAvailability.startDate = startDate.format(); + newAvailability.endDate = endDate.format(); + availabilities.push(newAvailability); + await this.bot.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); + return `Added availability of ${this.getAvailabilityString(newAvailability, timezone)} ${timezone}`; + } + + private async getValues(args: string[], username: string): Promise { + if (args[0]) { + if (this.isValidUsername(args[0])) { + username = args[0] + } + else { + return this.writeArgsErrorMessage(args); + } + } + + let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let defaultWorkLevel = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; + let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + if (timezone === '') { + return this.timezoneNotSetErrormessage(username); + } + if (availabilitiesString === '') { + return `${username} has not set their availability` + } + + let availabilities: Availability[] = JSON.parse(availabilitiesString); + + if (availabilities.length === 0) { + return `${username} has not set their availability` + } + return `Availability for user ${username}: +Default: ${defaultWorkLevel} +Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)}`; + } + + private async setValue(args: string[], username: string): Promise { + if (args[0] === this.configKeys.default && + this.isValidWorkLevel(args[1])) { + await this.bot.kvstore.put(this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]); + return `Your default availability has been set to ${args[1]}`; + } + else if (args[0] === this.configKeys.timezone && + this.isValidTimezone(args[1])) { + await this.bot.kvstore.put(this.teamName, this.nameSpaces.timezones, username, args[1]); + return `Your time zone has been updated to ${args[1]}`; + } + else { + return this.writeArgsErrorMessage(args); + } + } + + private async rmValue(args: string[], username: string): Promise { + let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + if (timezone === '') { + return this.timezoneNotSetErrormessage(username); + } + + if (args.length === 0) { + let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilities: Availability[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } + else { + return `${username} has not set their availability` + } + if (availabilities.length === 0) { + return `${username} has not set their availability` + } + + let defaultWorkLevel = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; + return `Which availability would you like to remove? +Default: ${defaultWorkLevel} +Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} +Respond with /avail rm #`; + } + else if (args[0] && !isNaN(Number(args[0]))) { + let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilities: Availability[] = []; + if (availabilitiesString !== '') { + availabilities = JSON.parse(availabilitiesString); + } + + let availabilityToRemove = availabilities[Number(args[0]) - 1]; + if (availabilityToRemove) { + availabilities.splice(Number(args[0]) - 1, 1); + } + else { + return this.writeArgsErrorMessage(args); + } + + await this.bot.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); + if (availabilityToRemove) { + return `Removed availability ${this.getAvailabilityString(availabilityToRemove, timezone)}`; + } + } + return this.writeArgsErrorMessage(args); + } + + private async msgReply(message: MsgSummary): Promise { + let args: string[] = message?.content?.text?.body.split(" ") || []; + if (args[1] === this.commandVerbs.addVerb) { + args.splice(0, 2); + return this.addValue(args, message?.sender?.username || ''); + } + else if (args[1] === this.commandVerbs.getVerb) { + args.splice(0, 2); + return this.getValues(args, message?.sender?.username || ''); + } + else if (args[1] === this.commandVerbs.setVerb) { + args.splice(0, 2); + return this.setValue(args, message?.sender?.username || ''); + } + else if (args[1] === this.commandVerbs.rmVerb) { + args.splice(0, 2); + return this.rmValue(args, message?.sender?.username || ''); + } + else { + let errorMessage: string = `Invalid command verb: ${args[2]}`; + console.error(errorMessage); + return errorMessage; + } + } +} diff --git a/src/index.ts b/src/index.ts index 02a895d..d635083 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,298 +1,14 @@ #!/usr/bin/env node -import Bot from 'keybase-bot' -import { MsgSummary } from 'keybase-bot/lib/types/chat1' -import moment, { Moment } from 'moment' -import momentTimezone from 'moment-timezone' +import { AgentAvailabilityBot } from './agentAvailabilityBot' -const assumedTime = '12:00'; -const bot = new Bot(); -const commandPrefix: string = '/avail '; -const commandVerbs = { - addVerb: 'add', - getVerb: 'get', - setVerb: 'set', - rmVerb: 'rm', -}; -const configKeys = { - default: 'default', - timezone: 'timezone' -}; -const dateFormat = 'M/D/YYYY'; -const inputDateFormat = 'M/D/YYYY HH:mm'; -const momentTimezoneNames = momentTimezone.tz.names(); -const nameSpaces = { - availabilities: 'AgentAvailability.Availabilities', - defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', - timezones: 'AgentAvailability.TimeZones', -} -const paperkey = process.env.KB_PAPERKEY; -const teamName = process.env.KB_TEAMNAME; -const username = process.env.KB_USERNAME; -// Regex for a valid integer 0-100 followed by a % -const workLevelRegex = /^(?:100|[1-9]?[0-9])%{1}$/ - -type Availability = { - startDate: string, - endDate: string, - workLevel: string -} - -const getAvailabilitiesString = (availabilities: Availability[], timezone: string): string => { - let availabilitiesString = ''; - availabilities.forEach((item, index) => { - let availability: Availability = { - startDate: item.startDate, - endDate: item.endDate, - workLevel: item.workLevel - } - availabilitiesString += `\r\n${index + 1}. ${getAvailabilityString(availability, timezone)}`; - }); - return availabilitiesString; -} - -const getAvailabilityString = (availability: Availability, timezone: string): string => { - let startDate = momentTimezone(availability.startDate).tz(timezone).format(dateFormat); - let endDate = momentTimezone(availability.endDate).tz(timezone).format(dateFormat); - return `[${startDate} - ${endDate}] ${availability.workLevel}`; -} - -const isValidDate = (date: string): boolean => { - let validatedDate: Moment = moment(date, dateFormat, true); - if (validatedDate.isValid()) { - return true; - } - return false; -} - -const isValidTimezone = (timezone: string): boolean => { - if (momentTimezoneNames.indexOf(timezone) > -1) { - return true; - } - return false; -} - -const isValidUsername = (username: string): boolean => { - return true; -} - -const isValidWorkLevel = (worklevel: string): boolean => { - if (workLevelRegex.test(worklevel)) { - return true - } - return false; -} - -const writeArgsErrorMessage = (args: string[]): string => { - let errorMessage: string = `Invalid arguments: ${args.toString()}`; - console.error(errorMessage); - return errorMessage; -} - -const timezoneNotSetErrormessage = (username: string): string => { - let errorMessage: string = `Timezone has not been set for user ${username}`; - console.error(errorMessage); - return errorMessage; -} - -async function addValue(args: string[], username: string): Promise { - let newAvailability: Availability = { - startDate: '', - endDate: '', - workLevel: '' - } - let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue - if (timezone === '') { - return timezoneNotSetErrormessage(username); - } - - if (!isValidWorkLevel(args[0]) && - !isValidDate(args[1])) { - return writeArgsErrorMessage(args); - } - if (args[2] && - !isValidDate(args[2])) { - return writeArgsErrorMessage(args); - } - - newAvailability.workLevel = args[0]; - newAvailability.startDate = args[1]; - if (!args[2]) { - newAvailability.endDate = args[1]; - } - else { - newAvailability.endDate = args[2]; - } - - let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let availabilities: object[] = []; - if (availabilitiesString !== '') { - availabilities = JSON.parse(availabilitiesString); - } - let startDate = momentTimezone(newAvailability.startDate + " " + assumedTime, inputDateFormat, timezone); - if (!startDate.isValid()) { - return `Invalid date: ${newAvailability.startDate}`; - } - let endDate = momentTimezone(newAvailability.endDate + " " + assumedTime, inputDateFormat, timezone); - if (!endDate.isValid()) { - return `Invalid date: ${newAvailability.endDate}`; - } - newAvailability.startDate = startDate.format(); - newAvailability.endDate = endDate.format(); - availabilities.push(newAvailability); - await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); - return `Added availability of ${getAvailabilityString(newAvailability, timezone)} ${timezone}`; -} - -async function getValues(args: string[], username: string): Promise { - if (args[0]) { - if (isValidUsername(args[0])) { - username = args[0] - } - else { - return writeArgsErrorMessage(args); - } - } - - let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; - let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; - if (timezone === '') { - return timezoneNotSetErrormessage(username); - } - if (availabilitiesString === '') { - return `${username} has not set their availability` - } - - let availabilities: Availability[] = JSON.parse(availabilitiesString); - - if (availabilities.length === 0) { - return `${username} has not set their availability` - } - return `Availability for user ${username}: - Default: ${defaultWorkLevel} - Time Zone: ${timezone} ${getAvailabilitiesString(availabilities, timezone)}`; - -} - -async function setValue(args: string[], username: string): Promise { - if (args[0] === configKeys.default && - isValidWorkLevel(args[1])) { - await bot.kvstore.put(teamName, nameSpaces.defaultWorkLevels, username, args[1]); - return `Your default availability has been set to ${args[1]}`; - } - else if (args[0] === configKeys.timezone && - isValidTimezone(args[1])) { - await bot.kvstore.put(teamName, nameSpaces.timezones, username, args[1]); - return `Your time zone has been updated to ${args[1]}`; - } - else { - return writeArgsErrorMessage(args); - } -} - -async function rmValue(args: string[], username: string): Promise { - let timezone = (await bot.kvstore.get(teamName, nameSpaces.timezones, username)).entryValue; - if (timezone === '') { - return timezoneNotSetErrormessage(username); - } - - if (args.length === 0) { - let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let availabilities: Availability[] = []; - if (availabilitiesString !== '') { - availabilities = JSON.parse(availabilitiesString); - } - else { - return `${username} has not set their availability` - } - if (availabilities.length === 0) { - return `${username} has not set their availability` - } - - let defaultWorkLevel = (await bot.kvstore.get(teamName, nameSpaces.defaultWorkLevels, username)).entryValue; - return `Which availability would you like to remove? - Default: ${defaultWorkLevel} - Time Zone: ${timezone} ${getAvailabilitiesString(availabilities, timezone)} - Respond with /avail rm #`; - } - else if (args[0] && !isNaN(Number(args[0]))) { - let availabilitiesString = (await bot.kvstore.get(teamName, nameSpaces.availabilities, username)).entryValue; - let availabilities: Availability[] = []; - if (availabilitiesString !== '') { - availabilities = JSON.parse(availabilitiesString); - } - - let availabilityToRemove = availabilities[Number(args[0]) - 1]; - if (availabilityToRemove) { - availabilities.splice(Number(args[0]) - 1, 1); - } - else { - return writeArgsErrorMessage(args); - } - - await bot.kvstore.put(teamName, nameSpaces.availabilities, username, JSON.stringify(availabilities)); - if (availabilityToRemove) { - return `Removed availability ${getAvailabilityString(availabilityToRemove, timezone)}`; - } - } - return writeArgsErrorMessage(args); -} - -async function msgReply(message: MsgSummary): Promise { - let args: string[] = message?.content?.text?.body.split(" ") || []; - if (args[1] === commandVerbs.addVerb) { - args.splice(0, 2); - return addValue(args, message?.sender?.username || ''); - } - else if (args[1] === commandVerbs.getVerb) { - args.splice(0, 2); - return getValues(args, message?.sender?.username || ''); - } - else if (args[1] === commandVerbs.setVerb) { - args.splice(0, 2); - return setValue(args, message?.sender?.username || ''); - } - else if (args[1] === commandVerbs.rmVerb) { - args.splice(0, 2); - return rmValue(args, message?.sender?.username || ''); - } - else { - let errorMessage: string = `Invalid command verb: ${args[2]}`; - console.error(errorMessage); - return errorMessage; - } -} +const agentAvailabilityBot = new AgentAvailabilityBot(); async function main() { - bot - .init(username || '', paperkey || '') - .then(() => { - console.log('Starting up', bot.myInfo()?.username, bot.myInfo()?.devicename); - console.log(`Watching for new messages to ${bot.myInfo()?.username} starting with ${commandPrefix}`); - async function onMessage(message: MsgSummary): Promise { - if (message?.content.type === 'text') { - const prefix = message?.content?.text?.body.slice(0, commandPrefix.length); - if (prefix === commandPrefix) { - const reply = { body: await msgReply(message) }; - bot.chat.send(message.conversationId, reply); - } - } - } - const onError = (e: any) => console.error(e); - bot.chat.watchAllChannelsForNewMessages(onMessage, onError); - }) - .catch((error: any) => { - console.error(error); - shutDown(); - }) -} - -function shutDown() { - bot.deinit().then(() => process.exit()); + agentAvailabilityBot.init(); } -process.on('SIGINT', shutDown); -process.on('SIGTERM', shutDown); +process.on('SIGINT', agentAvailabilityBot.deinit); +process.on('SIGTERM', agentAvailabilityBot.deinit); (async () => { try { From 5724ae07d7d027e45ad74d1b5cbb8e86e63dbf9e Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 9 May 2020 16:55:40 -0700 Subject: [PATCH 25/39] Got refactor into class working --- src/agentAvailabilityBot.ts | 110 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/agentAvailabilityBot.ts b/src/agentAvailabilityBot.ts index 00fb3a3..b9e0ddc 100644 --- a/src/agentAvailabilityBot.ts +++ b/src/agentAvailabilityBot.ts @@ -1,7 +1,8 @@ -import Bot from 'keybase-bot' import moment, { Moment } from 'moment' -import momentTimezone from 'moment-timezone' + +import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' +import momentTimezone from 'moment-timezone' type Availability = { startDate: string, @@ -9,41 +10,40 @@ type Availability = { workLevel: string } -export class AgentAvailabilityBot { - private assumedTime: string = '12:00'; - private bot: Bot = new Bot(); - private commandPrefix: string = '/avail '; - private commandVerbs: any = { +export class AgentAvailabilityBot extends Bot { + assumedTime: string = '12:00'; + commandPrefix: string = '/avail '; + commandVerbs: any = { addVerb: 'add', getVerb: 'get', setVerb: 'set', rmVerb: 'rm', } - private configKeys: any = { + configKeys: any = { default: 'default', timezone: 'timezone' }; - private dateFormat: string = 'M/D/YYYY'; - private inputDateFormat: string = 'M/D/YYYY HH:mm'; - private momentTimezoneNames: string[] = momentTimezone.tz.names(); - private nameSpaces: any = { + dateFormat: string = 'M/D/YYYY'; + inputDateFormat: string = 'M/D/YYYY HH:mm'; + momentTimezoneNames: string[] = momentTimezone.tz.names(); + nameSpaces: any = { availabilities: 'AgentAvailability.Availabilities', defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', timezones: 'AgentAvailability.TimeZones', } - private paperkey: string | undefined = process.env.KB_PAPERKEY; - private teamName: string | undefined = process.env.KB_TEAMNAME; - private username: string | undefined = process.env.KB_USERNAME; + paperkey: string | undefined = process.env.KB_PAPERKEY; + teamName: string | undefined = process.env.KB_TEAMNAME; + username: string | undefined = process.env.KB_USERNAME; // Regex for a valid integer 0-100 followed by a % - private workLevelRegex: RegExp = /^(?:100|[1-9]?[0-9])%{1}$/ + workLevelRegex: RegExp = /^(?:100|[1-9]?[0-9])%{1}$/ constructor() { - - } + super(); + this.initBot(); + } - public init() { - this.bot - .init(this.username || '', this.paperkey || '') + initBot() { + Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) .then(this.startUp) .catch((error: any) => { console.error(error); @@ -51,27 +51,27 @@ export class AgentAvailabilityBot { .then(this.deinit); } - public deinit() { - this.bot.deinit().then(() => process.exit()); + deinitBot() { + Bot.prototype.deinit.call(this).then(() => process.exit()); } - private startUp() { - console.log('Starting up', this.bot.myInfo()?.username, this.bot.myInfo()?.devicename); - console.log(`Watching for new messages to ${this.bot.myInfo()?.username} starting with ${this.commandPrefix}`); + startUp() { + console.log('Starting up', Bot.prototype.myInfo.call(this)?.username, Bot.prototype.myInfo.call(this)?.devicename); + console.log(`Watching for new messages to ${Bot.prototype.myInfo.call(this)?.username} starting with ${this.commandPrefix}`); const onError = (e: any) => console.error(e); - this.bot.chat.watchAllChannelsForNewMessages(this.onMessage, onError); + Bot.prototype.chat.watchAllChannelsForNewMessages.apply(this, [this.onMessage, onError]); } - private async onMessage(message: MsgSummary): Promise { + async onMessage(message: MsgSummary): Promise { if (message?.content.type === 'text') { const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); if (prefix === this.commandPrefix) { const reply = { body: await this.msgReply(message) }; - this.bot.chat.send(message.conversationId, reply); + Bot.prototype.chat.send.apply(this, [message.conversationId, reply]); } } } - private getAvailabilitiesString(availabilities: Availability[], timezone: string): string { + getAvailabilitiesString(availabilities: Availability[], timezone: string): string { let availabilitiesString = ''; availabilities.forEach((item, index) => { let availability: Availability = { @@ -84,13 +84,13 @@ export class AgentAvailabilityBot { return availabilitiesString; } - private getAvailabilityString(availability: Availability, timezone: string): string { + getAvailabilityString(availability: Availability, timezone: string): string { let startDate = momentTimezone(availability.startDate).tz(timezone).format(this.dateFormat); let endDate = momentTimezone(availability.endDate).tz(timezone).format(this.dateFormat); return `[${startDate} - ${endDate}] ${availability.workLevel}`; } - private isValidDate(date: string): boolean { + isValidDate(date: string): boolean { let validatedDate: Moment = moment(date, this.dateFormat, true); if (validatedDate.isValid()) { return true; @@ -98,43 +98,43 @@ export class AgentAvailabilityBot { return false; } - private isValidTimezone(timezone: string): boolean { + isValidTimezone(timezone: string): boolean { if (this.momentTimezoneNames.indexOf(timezone) > -1) { return true; } return false; } - private isValidUsername(username: string): boolean { + isValidUsername(username: string): boolean { return true; } - private isValidWorkLevel(worklevel: string): boolean { + isValidWorkLevel(worklevel: string): boolean { if (this.workLevelRegex.test(worklevel)) { return true } return false; } - private writeArgsErrorMessage(args: string[]): string { + writeArgsErrorMessage(args: string[]): string { let errorMessage: string = `Invalid arguments: ${args.toString()}`; console.error(errorMessage); return errorMessage; } - private timezoneNotSetErrormessage(username: string): string { + timezoneNotSetErrormessage(username: string): string { let errorMessage: string = `Timezone has not been set for user ${username}`; console.error(errorMessage); return errorMessage; } - private async addValue(args: string[], username: string): Promise { + async addValue(args: string[], username: string): Promise { let newAvailability: Availability = { startDate: '', endDate: '', workLevel: '' } - let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue + let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue if (timezone === '') { return this.timezoneNotSetErrormessage(username); } @@ -157,7 +157,7 @@ export class AgentAvailabilityBot { newAvailability.endDate = args[2]; } - let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; let availabilities: object[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -173,11 +173,11 @@ export class AgentAvailabilityBot { newAvailability.startDate = startDate.format(); newAvailability.endDate = endDate.format(); availabilities.push(newAvailability); - await this.bot.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); + await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)]); return `Added availability of ${this.getAvailabilityString(newAvailability, timezone)} ${timezone}`; } - private async getValues(args: string[], username: string): Promise { + async getValues(args: string[], username: string): Promise { if (args[0]) { if (this.isValidUsername(args[0])) { username = args[0] @@ -187,9 +187,9 @@ export class AgentAvailabilityBot { } } - let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; - let defaultWorkLevel = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; - let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; + let defaultWorkLevel = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username])).entryValue; + let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue; if (timezone === '') { return this.timezoneNotSetErrormessage(username); } @@ -207,15 +207,15 @@ Default: ${defaultWorkLevel} Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)}`; } - private async setValue(args: string[], username: string): Promise { + async setValue(args: string[], username: string): Promise { if (args[0] === this.configKeys.default && this.isValidWorkLevel(args[1])) { - await this.bot.kvstore.put(this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]); + await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]]); return `Your default availability has been set to ${args[1]}`; } else if (args[0] === this.configKeys.timezone && this.isValidTimezone(args[1])) { - await this.bot.kvstore.put(this.teamName, this.nameSpaces.timezones, username, args[1]); + await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.timezones, username, args[1]]); return `Your time zone has been updated to ${args[1]}`; } else { @@ -223,14 +223,14 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} } } - private async rmValue(args: string[], username: string): Promise { - let timezone = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + async rmValue(args: string[], username: string): Promise { + let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue; if (timezone === '') { return this.timezoneNotSetErrormessage(username); } if (args.length === 0) { - let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -242,14 +242,14 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} return `${username} has not set their availability` } - let defaultWorkLevel = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; + let defaultWorkLevel = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username])).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { - let availabilitiesString = (await this.bot.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -263,7 +263,7 @@ Respond with /avail rm #`; return this.writeArgsErrorMessage(args); } - await this.bot.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); + await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)]); if (availabilityToRemove) { return `Removed availability ${this.getAvailabilityString(availabilityToRemove, timezone)}`; } @@ -271,7 +271,7 @@ Respond with /avail rm #`; return this.writeArgsErrorMessage(args); } - private async msgReply(message: MsgSummary): Promise { + async msgReply(message: MsgSummary): Promise { let args: string[] = message?.content?.text?.body.split(" ") || []; if (args[1] === this.commandVerbs.addVerb) { args.splice(0, 2); From c046559a9b552d809d94dac6d23939b4aa0ac569 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 9 May 2020 17:07:06 -0700 Subject: [PATCH 26/39] Add additional tasks to do and reprioritize a little bit --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5aa9277..67ec4ad 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,12 @@ User: /avail rm 1 Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` -## Development tasks to do -* Deploy +## Tasks to do +* Refactor +* Setting up test users to test multiple users functionality +* Change /avail get to get all users' availability +* Add /avail me for getting your own user's availability +* Add permissions to limit it to to dOrg team * Add conversion to a specified time zone, example: ``` User: /avail get userb America/Los_Angeles @@ -98,6 +102,8 @@ Bot: Usage: /avail add [workLevel%] [MM/DD/YYYY] [MM/DD/YYYY] Examples: /avail add 0% 7/10/2020 7/30/2020 /avail add 50% 7/10/2020 ``` +* Deploy +* Make proposal to dOrg DAO for bounty completion * (Optional) Add CI/CD * (Optional) Figure out a simple way to validate keybase usernames: May need to add the [Go client](https://github.com/keybase/client) to project or implement own [user endpoint call.](https://keybase.io/docs/api/1.0/call/user/lookup) From 3c3a7062d016b1e9971a05f6c2e57c143feb9875 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 9 May 2020 17:39:34 -0700 Subject: [PATCH 27/39] Work on fixing super class method calls --- src/agentAvailabilityBot.ts | 23 +++++++++++++---------- src/index.ts | 1 + 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/agentAvailabilityBot.ts b/src/agentAvailabilityBot.ts index b9e0ddc..df9b943 100644 --- a/src/agentAvailabilityBot.ts +++ b/src/agentAvailabilityBot.ts @@ -39,27 +39,30 @@ export class AgentAvailabilityBot extends Bot { constructor() { super(); - this.initBot(); } - initBot() { - Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) - .then(this.startUp) + init(): Promise { + return Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) + .then(() => { + this.startUp(); + }) .catch((error: any) => { console.error(error); }) - .then(this.deinit); + .then(() => { + this.deinit(); + }) } - deinitBot() { - Bot.prototype.deinit.call(this).then(() => process.exit()); + deinit(): Promise { + return Bot.prototype.deinit.call(this).then(() => process.exit()); } startUp() { - console.log('Starting up', Bot.prototype.myInfo.call(this)?.username, Bot.prototype.myInfo.call(this)?.devicename); - console.log(`Watching for new messages to ${Bot.prototype.myInfo.call(this)?.username} starting with ${this.commandPrefix}`); + console.log('Starting up', this.myInfo.call(this)?.username, this.myInfo.call(this)?.devicename); + console.log(`Watching for new messages to ${this.myInfo.call(this)?.username} starting with ${this.commandPrefix}`); const onError = (e: any) => console.error(e); - Bot.prototype.chat.watchAllChannelsForNewMessages.apply(this, [this.onMessage, onError]); + this.chat.watchAllChannelsForNewMessages.apply(this, [this.onMessage, onError]); } async onMessage(message: MsgSummary): Promise { if (message?.content.type === 'text') { diff --git a/src/index.ts b/src/index.ts index d635083..9b2198f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node + import { AgentAvailabilityBot } from './agentAvailabilityBot' const agentAvailabilityBot = new AgentAvailabilityBot(); From d3fb08264325b8c533846228d2960b1f36359342 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 16 May 2020 09:50:20 -0700 Subject: [PATCH 28/39] Fix issues referencing parent class and 'this' --- src/agentAvailabilityBot.ts | 51 ++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/agentAvailabilityBot.ts b/src/agentAvailabilityBot.ts index df9b943..08afbac 100644 --- a/src/agentAvailabilityBot.ts +++ b/src/agentAvailabilityBot.ts @@ -42,34 +42,43 @@ export class AgentAvailabilityBot extends Bot { } init(): Promise { - return Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) - .then(() => { - this.startUp(); + return Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) + .then(() => { + this.startUp(); }) .catch((error: any) => { console.error(error); - }) - .then(() => { this.deinit(); }) } deinit(): Promise { + console.log('Shutting down...'); return Bot.prototype.deinit.call(this).then(() => process.exit()); } startUp() { - console.log('Starting up', this.myInfo.call(this)?.username, this.myInfo.call(this)?.devicename); + console.log('Starting up...', this.myInfo.call(this)?.username, this.myInfo.call(this)?.devicename); console.log(`Watching for new messages to ${this.myInfo.call(this)?.username} starting with ${this.commandPrefix}`); const onError = (e: any) => console.error(e); - this.chat.watchAllChannelsForNewMessages.apply(this, [this.onMessage, onError]); + const onMessage = async (message: MsgSummary) => { + if (message?.content.type === 'text') { + const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); + if (prefix === this.commandPrefix) { + const reply = { body: await this.msgReply(message) }; + this.chat.send(message.conversationId, reply); + } + } + } + this.chat.watchAllChannelsForNewMessages(onMessage, onError); } + async onMessage(message: MsgSummary): Promise { if (message?.content.type === 'text') { const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); if (prefix === this.commandPrefix) { const reply = { body: await this.msgReply(message) }; - Bot.prototype.chat.send.apply(this, [message.conversationId, reply]); + this.chat.send(message.conversationId, reply); } } } @@ -137,7 +146,7 @@ export class AgentAvailabilityBot extends Bot { endDate: '', workLevel: '' } - let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue if (timezone === '') { return this.timezoneNotSetErrormessage(username); } @@ -160,7 +169,7 @@ export class AgentAvailabilityBot extends Bot { newAvailability.endDate = args[2]; } - let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; let availabilities: object[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -176,7 +185,7 @@ export class AgentAvailabilityBot extends Bot { newAvailability.startDate = startDate.format(); newAvailability.endDate = endDate.format(); availabilities.push(newAvailability); - await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)]); + await this.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); return `Added availability of ${this.getAvailabilityString(newAvailability, timezone)} ${timezone}`; } @@ -190,9 +199,9 @@ export class AgentAvailabilityBot extends Bot { } } - let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; - let defaultWorkLevel = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username])).entryValue; - let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; if (timezone === '') { return this.timezoneNotSetErrormessage(username); } @@ -213,12 +222,12 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} async setValue(args: string[], username: string): Promise { if (args[0] === this.configKeys.default && this.isValidWorkLevel(args[1])) { - await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]]); + await this.kvstore.put(this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]); return `Your default availability has been set to ${args[1]}`; } else if (args[0] === this.configKeys.timezone && this.isValidTimezone(args[1])) { - await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.timezones, username, args[1]]); + await this.kvstore.put(this.teamName, this.nameSpaces.timezones, username, args[1]); return `Your time zone has been updated to ${args[1]}`; } else { @@ -227,13 +236,13 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} } async rmValue(args: string[], username: string): Promise { - let timezone = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.timezones, username])).entryValue; + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; if (timezone === '') { return this.timezoneNotSetErrormessage(username); } if (args.length === 0) { - let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -245,14 +254,14 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} return `${username} has not set their availability` } - let defaultWorkLevel = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.defaultWorkLevels, username])).entryValue; + let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { - let availabilitiesString = (await Bot.prototype.kvstore.get.apply(this, [this.teamName, this.nameSpaces.availabilities, username])).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -266,7 +275,7 @@ Respond with /avail rm #`; return this.writeArgsErrorMessage(args); } - await Bot.prototype.kvstore.put.apply(this, [this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)]); + await this.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); if (availabilityToRemove) { return `Removed availability ${this.getAvailabilityString(availabilityToRemove, timezone)}`; } From 43f3e395daf7d15a6336cff01739f7c713c84223 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 16 May 2020 10:44:09 -0700 Subject: [PATCH 29/39] Ensure bot shuts down gracefully --- src/agentAvailabilityBot.ts | 23 ++++++++++++----------- src/index.ts | 6 +++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/agentAvailabilityBot.ts b/src/agentAvailabilityBot.ts index 08afbac..ebd00ee 100644 --- a/src/agentAvailabilityBot.ts +++ b/src/agentAvailabilityBot.ts @@ -41,20 +41,21 @@ export class AgentAvailabilityBot extends Bot { super(); } - init(): Promise { - return Bot.prototype.init.apply(this, [this.username || '', this.paperkey || '']) - .then(() => { - this.startUp(); - }) - .catch((error: any) => { - console.error(error); - this.deinit(); - }) + async initBot(): Promise { + try { + await this.init(this.username || '', this.paperkey || ''); + this.startUp(); + } + catch (error) { + console.error(error); + await this.deinit(); + } } - deinit(): Promise { + async deinitBot(): Promise { console.log('Shutting down...'); - return Bot.prototype.deinit.call(this).then(() => process.exit()); + await this.deinit(); + return process.exit(); } startUp() { diff --git a/src/index.ts b/src/index.ts index 9b2198f..1f9d4d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,11 +5,11 @@ import { AgentAvailabilityBot } from './agentAvailabilityBot' const agentAvailabilityBot = new AgentAvailabilityBot(); async function main() { - agentAvailabilityBot.init(); + await agentAvailabilityBot.initBot(); } -process.on('SIGINT', agentAvailabilityBot.deinit); -process.on('SIGTERM', agentAvailabilityBot.deinit); +process.on('SIGINT', agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot)); +process.on('SIGTERM', agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot)); (async () => { try { From a2da0a0dfee4a3f0b481478b97a79090cc776efd Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 16 May 2020 11:26:31 -0700 Subject: [PATCH 30/39] Move configuration to additional environment variables, reorganize files --- README.md | 19 +- .../AgentAvailabilityBot.ts} | 176 ++++++++---------- src/bot/Availability.ts | 12 ++ src/index.ts | 2 +- 4 files changed, 111 insertions(+), 98 deletions(-) rename src/{agentAvailabilityBot.ts => bot/AgentAvailabilityBot.ts} (58%) create mode 100644 src/bot/Availability.ts diff --git a/README.md b/README.md index 67ec4ad..d286951 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,22 @@ Set up these two files in a `.vscode` folder at the root of the Git repository t "configurations": [ { "env": { - "KB_USERNAME": "keybase_username", - "KB_PAPERKEY": "keybase_paperkey", - "KB_TEAMNAME": "keybase_teamname" + "KEYBASE_AGENTAVAILABILITYBOT_USERNAME": "keybase_username", + "KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY": "keybase_paperkey", + "KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME": "keybase_teamname", + "KEYBASE_AGENTAVAILABILITYBOT_COMMANDPREFIX": "/avail ", + "KEYBASE_AGENTAVAILABILITYBOT_ASSUMEDTIME": "12:00", + "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_ADD": "add", + "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_GET": "get", + "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_SET": "set", + "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_RM": "rm", + "KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_DEFAULT": "default", + "KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_TIMEZONE": "timezone", + "KEYBASE_AGENTAVAILABILITYBOT_DATEFORMAT": "M/D/YYYY", + "KEYBASE_AGENTAVAILABILITYBOT_INPUTDATEFORMAT": "M/D/YYYY HH:mm", + "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_AVAILABILITIES": "AgentAvailability.Availabilities", + "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_DEFAULT": "AgentAvailability.DefaultWorkLevels", + "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_TIMEZONES": "AgentAvailability.TimeZones", }, "name": "Launch Program", "outFiles": [ diff --git a/src/agentAvailabilityBot.ts b/src/bot/AgentAvailabilityBot.ts similarity index 58% rename from src/agentAvailabilityBot.ts rename to src/bot/AgentAvailabilityBot.ts index ebd00ee..a11aaf7 100644 --- a/src/agentAvailabilityBot.ts +++ b/src/bot/AgentAvailabilityBot.ts @@ -1,50 +1,48 @@ import moment, { Moment } from 'moment' +import Availability from './Availability' import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' import momentTimezone from 'moment-timezone' -type Availability = { - startDate: string, - endDate: string, - workLevel: string -} - export class AgentAvailabilityBot extends Bot { - assumedTime: string = '12:00'; - commandPrefix: string = '/avail '; - commandVerbs: any = { - addVerb: 'add', - getVerb: 'get', - setVerb: 'set', - rmVerb: 'rm', + assumedTime: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_ASSUMEDTIME; + commandPrefix: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDPREFIX; + commandVerbs: { [id: string]: string | undefined; } = { + add: process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_ADD, + get: process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_GET, + set: process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_SET, + rm: process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_RM, } - configKeys: any = { - default: 'default', - timezone: 'timezone' + configKeys: { [id: string]: string | undefined; } = { + default: process.env.KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_DEFAULT, + timezone: process.env.KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_TIMEZONE, }; - dateFormat: string = 'M/D/YYYY'; - inputDateFormat: string = 'M/D/YYYY HH:mm'; + dateFormat: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_DATEFORMAT; + inputDateFormat: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_INPUTDATEFORMAT; momentTimezoneNames: string[] = momentTimezone.tz.names(); - nameSpaces: any = { - availabilities: 'AgentAvailability.Availabilities', - defaultWorkLevels: 'AgentAvailability.DefaultWorkLevels', - timezones: 'AgentAvailability.TimeZones', + nameSpaces: { [id: string]: string | undefined; } = { + availabilities: process.env.KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_AVAILABILITIES, + defaultWorkLevels: process.env.KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_DEFAULT, + timezones: process.env.KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_TIMEZONES, } - paperkey: string | undefined = process.env.KB_PAPERKEY; - teamName: string | undefined = process.env.KB_TEAMNAME; - username: string | undefined = process.env.KB_USERNAME; + paperkey: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY; + teamName: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME; + username: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_USERNAME; // Regex for a valid integer 0-100 followed by a % workLevelRegex: RegExp = /^(?:100|[1-9]?[0-9])%{1}$/ - constructor() { + constructor(workLevelRegex?: RegExp) { super(); + if (workLevelRegex) { + this.workLevelRegex = workLevelRegex; + } } async initBot(): Promise { try { await this.init(this.username || '', this.paperkey || ''); - this.startUp(); + this._startUp(); } catch (error) { console.error(error); @@ -58,15 +56,15 @@ export class AgentAvailabilityBot extends Bot { return process.exit(); } - startUp() { + _startUp() { console.log('Starting up...', this.myInfo.call(this)?.username, this.myInfo.call(this)?.devicename); console.log(`Watching for new messages to ${this.myInfo.call(this)?.username} starting with ${this.commandPrefix}`); const onError = (e: any) => console.error(e); const onMessage = async (message: MsgSummary) => { if (message?.content.type === 'text') { - const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); + const prefix = message?.content?.text?.body.slice(0, this.commandPrefix?.length); if (prefix === this.commandPrefix) { - const reply = { body: await this.msgReply(message) }; + const reply = { body: await this._msgReply(message) }; this.chat.send(message.conversationId, reply); } } @@ -74,17 +72,7 @@ export class AgentAvailabilityBot extends Bot { this.chat.watchAllChannelsForNewMessages(onMessage, onError); } - async onMessage(message: MsgSummary): Promise { - if (message?.content.type === 'text') { - const prefix = message?.content?.text?.body.slice(0, this.commandPrefix.length); - if (prefix === this.commandPrefix) { - const reply = { body: await this.msgReply(message) }; - this.chat.send(message.conversationId, reply); - } - } - } - - getAvailabilitiesString(availabilities: Availability[], timezone: string): string { + _getAvailabilitiesString(availabilities: Availability[], timezone: string): string { let availabilitiesString = ''; availabilities.forEach((item, index) => { let availability: Availability = { @@ -92,18 +80,18 @@ export class AgentAvailabilityBot extends Bot { endDate: item.endDate, workLevel: item.workLevel } - availabilitiesString += `\r\n${index + 1}. ${this.getAvailabilityString(availability, timezone)}`; + availabilitiesString += `\r\n${index + 1}. ${this._getAvailabilityString(availability, timezone)}`; }); return availabilitiesString; } - getAvailabilityString(availability: Availability, timezone: string): string { + _getAvailabilityString(availability: Availability, timezone: string): string { let startDate = momentTimezone(availability.startDate).tz(timezone).format(this.dateFormat); let endDate = momentTimezone(availability.endDate).tz(timezone).format(this.dateFormat); return `[${startDate} - ${endDate}] ${availability.workLevel}`; } - isValidDate(date: string): boolean { + _isValidDate(date: string): boolean { let validatedDate: Moment = moment(date, this.dateFormat, true); if (validatedDate.isValid()) { return true; @@ -111,54 +99,54 @@ export class AgentAvailabilityBot extends Bot { return false; } - isValidTimezone(timezone: string): boolean { + _isValidTimezone(timezone: string): boolean { if (this.momentTimezoneNames.indexOf(timezone) > -1) { return true; } return false; } - isValidUsername(username: string): boolean { + _isValidUsername(username: string): boolean { return true; } - isValidWorkLevel(worklevel: string): boolean { + _isValidWorkLevel(worklevel: string): boolean { if (this.workLevelRegex.test(worklevel)) { return true } return false; } - writeArgsErrorMessage(args: string[]): string { + _writeArgsErrorMessage(args: string[]): string { let errorMessage: string = `Invalid arguments: ${args.toString()}`; console.error(errorMessage); return errorMessage; } - timezoneNotSetErrormessage(username: string): string { + _timezoneNotSetErrormessage(username: string): string { let errorMessage: string = `Timezone has not been set for user ${username}`; console.error(errorMessage); return errorMessage; } - async addValue(args: string[], username: string): Promise { + async _addValue(args: string[], username: string): Promise { let newAvailability: Availability = { startDate: '', endDate: '', workLevel: '' } - let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones || '', username)).entryValue if (timezone === '') { - return this.timezoneNotSetErrormessage(username); + return this._timezoneNotSetErrormessage(username); } - if (!this.isValidWorkLevel(args[0]) && - !this.isValidDate(args[1])) { - return this.writeArgsErrorMessage(args); + if (!this._isValidWorkLevel(args[0]) && + !this._isValidDate(args[1])) { + return this._writeArgsErrorMessage(args); } if (args[2] && - !this.isValidDate(args[2])) { - return this.writeArgsErrorMessage(args); + !this._isValidDate(args[2])) { + return this._writeArgsErrorMessage(args); } newAvailability.workLevel = args[0]; @@ -170,7 +158,7 @@ export class AgentAvailabilityBot extends Bot { newAvailability.endDate = args[2]; } - let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities || '', username)).entryValue; let availabilities: object[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -186,25 +174,25 @@ export class AgentAvailabilityBot extends Bot { newAvailability.startDate = startDate.format(); newAvailability.endDate = endDate.format(); availabilities.push(newAvailability); - await this.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); - return `Added availability of ${this.getAvailabilityString(newAvailability, timezone)} ${timezone}`; + await this.kvstore.put(this.teamName, this.nameSpaces.availabilities || '', username, JSON.stringify(availabilities)); + return `Added availability of ${this._getAvailabilityString(newAvailability, timezone)} ${timezone}`; } - async getValues(args: string[], username: string): Promise { + async _getValues(args: string[], username: string): Promise { if (args[0]) { - if (this.isValidUsername(args[0])) { + if (this._isValidUsername(args[0])) { username = args[0] } else { - return this.writeArgsErrorMessage(args); + return this._writeArgsErrorMessage(args); } } - let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; - let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; - let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities || '', username)).entryValue; + let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels || '', username)).entryValue; + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones || '', username)).entryValue; if (timezone === '') { - return this.timezoneNotSetErrormessage(username); + return this._timezoneNotSetErrormessage(username); } if (availabilitiesString === '') { return `${username} has not set their availability` @@ -217,33 +205,33 @@ export class AgentAvailabilityBot extends Bot { } return `Availability for user ${username}: Default: ${defaultWorkLevel} -Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)}`; +Time Zone: ${timezone} ${this._getAvailabilitiesString(availabilities, timezone)}`; } - async setValue(args: string[], username: string): Promise { + async _setValue(args: string[], username: string): Promise { if (args[0] === this.configKeys.default && - this.isValidWorkLevel(args[1])) { - await this.kvstore.put(this.teamName, this.nameSpaces.defaultWorkLevels, username, args[1]); + this._isValidWorkLevel(args[1])) { + await this.kvstore.put(this.teamName, this.nameSpaces.defaultWorkLevels || '', username, args[1]); return `Your default availability has been set to ${args[1]}`; } else if (args[0] === this.configKeys.timezone && - this.isValidTimezone(args[1])) { - await this.kvstore.put(this.teamName, this.nameSpaces.timezones, username, args[1]); + this._isValidTimezone(args[1])) { + await this.kvstore.put(this.teamName, this.nameSpaces.timezones || '', username, args[1]); return `Your time zone has been updated to ${args[1]}`; } else { - return this.writeArgsErrorMessage(args); + return this._writeArgsErrorMessage(args); } } - async rmValue(args: string[], username: string): Promise { - let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones, username)).entryValue; + async _rmValue(args: string[], username: string): Promise { + let timezone = (await this.kvstore.get(this.teamName, this.nameSpaces.timezones || '', username)).entryValue; if (timezone === '') { - return this.timezoneNotSetErrormessage(username); + return this._timezoneNotSetErrormessage(username); } if (args.length === 0) { - let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities || '', username)).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -255,14 +243,14 @@ Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} return `${username} has not set their availability` } - let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels, username)).entryValue; + let defaultWorkLevel = (await this.kvstore.get(this.teamName, this.nameSpaces.defaultWorkLevels || '', username)).entryValue; return `Which availability would you like to remove? Default: ${defaultWorkLevel} -Time Zone: ${timezone} ${this.getAvailabilitiesString(availabilities, timezone)} +Time Zone: ${timezone} ${this._getAvailabilitiesString(availabilities, timezone)} Respond with /avail rm #`; } else if (args[0] && !isNaN(Number(args[0]))) { - let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities, username)).entryValue; + let availabilitiesString = (await this.kvstore.get(this.teamName, this.nameSpaces.availabilities || '', username)).entryValue; let availabilities: Availability[] = []; if (availabilitiesString !== '') { availabilities = JSON.parse(availabilitiesString); @@ -273,34 +261,34 @@ Respond with /avail rm #`; availabilities.splice(Number(args[0]) - 1, 1); } else { - return this.writeArgsErrorMessage(args); + return this._writeArgsErrorMessage(args); } - await this.kvstore.put(this.teamName, this.nameSpaces.availabilities, username, JSON.stringify(availabilities)); + await this.kvstore.put(this.teamName, this.nameSpaces.availabilities || '', username, JSON.stringify(availabilities)); if (availabilityToRemove) { - return `Removed availability ${this.getAvailabilityString(availabilityToRemove, timezone)}`; + return `Removed availability ${this._getAvailabilityString(availabilityToRemove, timezone)}`; } } - return this.writeArgsErrorMessage(args); + return this._writeArgsErrorMessage(args); } - async msgReply(message: MsgSummary): Promise { + async _msgReply(message: MsgSummary): Promise { let args: string[] = message?.content?.text?.body.split(" ") || []; - if (args[1] === this.commandVerbs.addVerb) { + if (args[1] === this.commandVerbs.add) { args.splice(0, 2); - return this.addValue(args, message?.sender?.username || ''); + return this._addValue(args, message?.sender?.username || ''); } - else if (args[1] === this.commandVerbs.getVerb) { + else if (args[1] === this.commandVerbs.get) { args.splice(0, 2); - return this.getValues(args, message?.sender?.username || ''); + return this._getValues(args, message?.sender?.username || ''); } - else if (args[1] === this.commandVerbs.setVerb) { + else if (args[1] === this.commandVerbs.set) { args.splice(0, 2); - return this.setValue(args, message?.sender?.username || ''); + return this._setValue(args, message?.sender?.username || ''); } - else if (args[1] === this.commandVerbs.rmVerb) { + else if (args[1] === this.commandVerbs.rm) { args.splice(0, 2); - return this.rmValue(args, message?.sender?.username || ''); + return this._rmValue(args, message?.sender?.username || ''); } else { let errorMessage: string = `Invalid command verb: ${args[2]}`; diff --git a/src/bot/Availability.ts b/src/bot/Availability.ts new file mode 100644 index 0000000..2fc5cf1 --- /dev/null +++ b/src/bot/Availability.ts @@ -0,0 +1,12 @@ +export default class Availability +{ + startDate: string; + endDate: string; + workLevel: string; + + constructor(startDate: string, endDate: string, workLevel: string) { + this.startDate = startDate; + this.endDate = endDate; + this.workLevel = workLevel; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1f9d4d1..b60d43a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { AgentAvailabilityBot } from './agentAvailabilityBot' +import { AgentAvailabilityBot } from './bot/AgentAvailabilityBot' const agentAvailabilityBot = new AgentAvailabilityBot(); From 2c1e63f822b297c801623bb52969c10c44bb2048 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 16 May 2020 13:44:18 -0700 Subject: [PATCH 31/39] Minor tweaks --- README.md | 9 ++++----- src/bot/AgentAvailabilityBot.ts | 2 +- src/index.ts | 2 +- src/package.json | 6 +++++- src/yarn.lock | 5 +++++ 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d286951..e54d92a 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,10 @@ Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` ## Tasks to do -* Refactor -* Setting up test users to test multiple users functionality * Change /avail get to get all users' availability * Add /avail me for getting your own user's availability * Add permissions to limit it to to dOrg team +* Setup test users to test multiple users functionality * Add conversion to a specified time zone, example: ``` User: /avail get userb America/Los_Angeles @@ -86,7 +85,7 @@ Time Zone: America/Los_Angeles - [5/02/2020 - 5/10/2020] 75% ``` Uses specified time zone "America/Los_Angeles" instead of userb's time zone of "America/New_York" -* (Optional) Add a help verb to display docs for different commands, example: +* Add a help verb to display docs for different commands, example: ``` User: /avail help Bot: Usage: /avail [verb] [parameter1] [parameter2] [parameter3] @@ -104,8 +103,8 @@ Examples: /avail add 0% 7/10/2020 7/30/2020 ``` * Deploy * Make proposal to dOrg DAO for bounty completion -* (Optional) Add CI/CD -* (Optional) Figure out a simple way to validate keybase usernames: +* Add CI/CD +* Figure out a simple way to validate keybase usernames: May need to add the [Go client](https://github.com/keybase/client) to project or implement own [user endpoint call.](https://keybase.io/docs/api/1.0/call/user/lookup) ## Running locally diff --git a/src/bot/AgentAvailabilityBot.ts b/src/bot/AgentAvailabilityBot.ts index a11aaf7..450eb57 100644 --- a/src/bot/AgentAvailabilityBot.ts +++ b/src/bot/AgentAvailabilityBot.ts @@ -5,7 +5,7 @@ import Bot from 'keybase-bot' import { MsgSummary } from 'keybase-bot/lib/types/chat1' import momentTimezone from 'moment-timezone' -export class AgentAvailabilityBot extends Bot { +export default class AgentAvailabilityBot extends Bot { assumedTime: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_ASSUMEDTIME; commandPrefix: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_COMMANDPREFIX; commandVerbs: { [id: string]: string | undefined; } = { diff --git a/src/index.ts b/src/index.ts index b60d43a..27e3aad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { AgentAvailabilityBot } from './bot/AgentAvailabilityBot' +import AgentAvailabilityBot from './bot/AgentAvailabilityBot' const agentAvailabilityBot = new AgentAvailabilityBot(); diff --git a/src/package.json b/src/package.json index 5c93aa1..a06f5a9 100644 --- a/src/package.json +++ b/src/package.json @@ -6,5 +6,9 @@ "keybase-bot": "^3.6.1", "moment": "^2.24.0", "moment-timezone": "^0.5.28" - } + }, + "devDependencies": { + "typescript": "^3.9.2" + }, + "license": "MIT" } diff --git a/src/yarn.lock b/src/yarn.lock index d6e7837..c2203aa 100644 --- a/src/yarn.lock +++ b/src/yarn.lock @@ -75,6 +75,11 @@ moment-timezone@^0.5.28: resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +typescript@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" + integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== + which@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From 946cd20942b05d430673d6f26bd0f24813564dad Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Fri, 22 May 2020 22:23:05 -0400 Subject: [PATCH 32/39] changes based on feedback --- .gitignore | 1 - .nvmrc | 1 + {src => .vscode}/tasks.json | 0 src/package.json => package.json | 3 +++ src/tsconfig.json => tsconfig.json | 8 ++++---- src/yarn.lock => yarn.lock | 0 6 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 .nvmrc rename {src => .vscode}/tasks.json (100%) rename src/package.json => package.json (87%) rename src/tsconfig.json => tsconfig.json (93%) rename src/yarn.lock => yarn.lock (100%) diff --git a/.gitignore b/.gitignore index d1ffb78..77c9374 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ _temp/ -.vscode .DS_Store gulp-tsc-tmp-* .gulp-tsc-tmp-* diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..dc49ba9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v10.16.3 \ No newline at end of file diff --git a/src/tasks.json b/.vscode/tasks.json similarity index 100% rename from src/tasks.json rename to .vscode/tasks.json diff --git a/src/package.json b/package.json similarity index 87% rename from src/package.json rename to package.json index a06f5a9..bc20198 100644 --- a/src/package.json +++ b/package.json @@ -1,4 +1,7 @@ { + "scripts": { + "build": "tsc" + }, "dependencies": { "@types/moment-timezone": "^0.5.13", "@types/node": "^13.11.1", diff --git a/src/tsconfig.json b/tsconfig.json similarity index 93% rename from src/tsconfig.json rename to tsconfig.json index 07d007e..b440529 100644 --- a/src/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./output", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "outDir": "./output", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ @@ -23,11 +23,11 @@ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* Enable strict null checks. */ + "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ diff --git a/src/yarn.lock b/yarn.lock similarity index 100% rename from src/yarn.lock rename to yarn.lock From 5cc7fc339682251c2216c031375973af4d2b018d Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Fri, 22 May 2020 23:47:06 -0400 Subject: [PATCH 33/39] changes based on feedback --- src/bot/Availability.ts | 10 ++-------- src/index.ts | 32 ++++++++++++++++++-------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/bot/Availability.ts b/src/bot/Availability.ts index 2fc5cf1..eb20dfb 100644 --- a/src/bot/Availability.ts +++ b/src/bot/Availability.ts @@ -1,12 +1,6 @@ -export default class Availability +export default interface Availability { startDate: string; endDate: string; workLevel: string; - - constructor(startDate: string, endDate: string, workLevel: string) { - this.startDate = startDate; - this.endDate = endDate; - this.workLevel = workLevel; - } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 27e3aad..3ff8a97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,25 @@ -#!/usr/bin/env node - import AgentAvailabilityBot from './bot/AgentAvailabilityBot' -const agentAvailabilityBot = new AgentAvailabilityBot(); - async function main() { + const agentAvailabilityBot = new AgentAvailabilityBot(); await agentAvailabilityBot.initBot(); + process.on('SIGINT', + agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot) + ); + process.on('SIGTERM', + agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot) + ); } -process.on('SIGINT', agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot)); -process.on('SIGTERM', agentAvailabilityBot.deinitBot.bind(agentAvailabilityBot)); +if (require.main === module) { + main() + .then(() => console.log('done')) + .catch(err => { + console.error(err); + process.exit(1); + }); +} -(async () => { - try { - var text = await main(); - console.log(text); - } catch (e) { - console.log(e) - } -})(); +export { + AgentAvailabilityBot +} From 7559f6b77a90533fe8b17795d1f7efd7e140b01c Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Fri, 22 May 2020 23:55:13 -0400 Subject: [PATCH 34/39] changes based on feedback --- src/bot/AgentAvailabilityBot.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/bot/AgentAvailabilityBot.ts b/src/bot/AgentAvailabilityBot.ts index 450eb57..f598c0b 100644 --- a/src/bot/AgentAvailabilityBot.ts +++ b/src/bot/AgentAvailabilityBot.ts @@ -274,6 +274,13 @@ Respond with /avail rm #`; async _msgReply(message: MsgSummary): Promise { let args: string[] = message?.content?.text?.body.split(" ") || []; + + if (args.length === 0) { + let errorMessage: string = `No command given.`; + console.error(errorMessage); + return errorMessage; + } + if (args[1] === this.commandVerbs.add) { args.splice(0, 2); return this._addValue(args, message?.sender?.username || ''); From a627c7e693d9570d02571e066ea145c72782db59 Mon Sep 17 00:00:00 2001 From: dOrgJelli Date: Mon, 20 Apr 2020 18:15:21 -0400 Subject: [PATCH 35/39] init --- .env.example | 16 ++++++ .gitignore | 3 ++ .nvmrc | 2 +- README.md | 140 +++++++++++++++++++-------------------------------- 4 files changed, 71 insertions(+), 90 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..508726c --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +KEYBASE_AGENTAVAILABILITYBOT_USERNAME=keybase_username, +KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY=keybase_paperkey, +KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME=keybase_teamname, +KEYBASE_AGENTAVAILABILITYBOT_COMMANDPREFIX=/avail , +KEYBASE_AGENTAVAILABILITYBOT_ASSUMEDTIME=12:00, +KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_ADD=add, +KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_GET=get, +KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_SET=set, +KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_RM=rm, +KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_DEFAULT=default, +KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_TIMEZONE=timezone, +KEYBASE_AGENTAVAILABILITYBOT_DATEFORMAT=M/D/YYYY, +KEYBASE_AGENTAVAILABILITYBOT_INPUTDATEFORMAT=M/D/YYYY HH:mm, +KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_AVAILABILITIES=AgentAvailability.Availabilities, +KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_DEFAULT=AgentAvailability.DefaultWorkLevels, +KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_TIMEZONES=AgentAvailability.TimeZones, \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77c9374..124fc50 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,6 @@ dist .yarn/unplugged .yarn/build-state.yml .pnp.* + +launch.json +tasks.json diff --git a/.nvmrc b/.nvmrc index dc49ba9..70047db 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10.16.3 \ No newline at end of file +v10.16.3 diff --git a/README.md b/README.md index e54d92a..807ec7b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # AgentAvailability -This Keybase bot is used to signal dOrg Agent availability. -Timezones must a valid Moment timezone name. -See examples in the zones properties [here](https://github.com/moment/moment-timezone/blob/develop/data/meta/latest.json). -Noon in the user's local is used as the assumed time of the provided availability date to make timezone conversions more consistent. - -## Example usage +A Keybase bot used to signal dOrg Agent availability. +## Usage +### Get Availability ``` User: /avail get +``` +``` Bot: Availability for user usera: Default: 50% Time Zone: America/New_York @@ -49,6 +48,10 @@ User: /avail set timezone America/New_York Bot: Your time zone has been updated to America/New_York ``` +**NOTE:** Timezones must a valid Moment timezone name. +See examples in the zones properties [here](https://github.com/moment/moment-timezone/blob/develop/data/meta/latest.json). +Noon in the user's local is used as the assumed time of the provided availability date to make timezone conversions more consistent. + ``` User: /avail add 0% 7/10/2020 7/30/2020 Bot: Added availability of 0% for 7/10/2020 7/30/2020 America/Los_Angeles @@ -68,7 +71,47 @@ User: /avail rm 1 Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ``` +## Running Locally + +1. Use the proper version of Node & NPM +```base +nvm use $(cat .nvmrc) +``` +2. Install dependencies +```bash +yarn +``` +3. + +## Debugging With VSCode + +Add this file to your `.vscode` folder at the root of the Git repository to debug within Visual Studio Code: + +`launch.json` ([documentation](https://go.microsoft.com/fwlink/?linkid=830387)) + +```json +{ + "configurations": [ + { + "name": "Launch Program", + "program": "${workspaceFolder}//src//output//index.js", + "request": "launch", + "smartStep": true, + "sourceMaps": true, + "type": "node" + } + ], + "version": "3.0.1" +} +``` + +More information on Node.js debugging within VSCode can be found [here](https://code.visualstudio.com/docs/nodejs/nodejs-debugging). + ## Tasks to do +* Add build process +* Add debugging +* Add testing +* Add better examples * Change /avail get to get all users' availability * Add /avail me for getting your own user's availability * Add permissions to limit it to to dOrg team @@ -103,87 +146,6 @@ Examples: /avail add 0% 7/10/2020 7/30/2020 ``` * Deploy * Make proposal to dOrg DAO for bounty completion -* Add CI/CD -* Figure out a simple way to validate keybase usernames: +* (Optional) Add CI/CD +* (Optional) Figure out a simple way to validate keybase usernames: May need to add the [Go client](https://github.com/keybase/client) to project or implement own [user endpoint call.](https://keybase.io/docs/api/1.0/call/user/lookup) - -## Running locally - -You will need to run yarn install in the `src` folder to get started: - -```bash -yarn install -``` - -## Debugging locally - -Set up these two files in a `.vscode` folder at the root of the Git repository to debug within Visual Studio Code: - -`launch.json` - replace example values in env's nested properties as appropriate - -```json -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "configurations": [ - { - "env": { - "KEYBASE_AGENTAVAILABILITYBOT_USERNAME": "keybase_username", - "KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY": "keybase_paperkey", - "KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME": "keybase_teamname", - "KEYBASE_AGENTAVAILABILITYBOT_COMMANDPREFIX": "/avail ", - "KEYBASE_AGENTAVAILABILITYBOT_ASSUMEDTIME": "12:00", - "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_ADD": "add", - "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_GET": "get", - "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_SET": "set", - "KEYBASE_AGENTAVAILABILITYBOT_COMMANDVERB_RM": "rm", - "KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_DEFAULT": "default", - "KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_TIMEZONE": "timezone", - "KEYBASE_AGENTAVAILABILITYBOT_DATEFORMAT": "M/D/YYYY", - "KEYBASE_AGENTAVAILABILITYBOT_INPUTDATEFORMAT": "M/D/YYYY HH:mm", - "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_AVAILABILITIES": "AgentAvailability.Availabilities", - "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_DEFAULT": "AgentAvailability.DefaultWorkLevels", - "KEYBASE_AGENTAVAILABILITYBOT_NAMESPACE_TIMEZONES": "AgentAvailability.TimeZones", - }, - "name": "Launch Program", - "outFiles": [ - "${workspaceFolder}/src/output/*.js" - ], - "outputCapture": "std", - "program": "${workspaceFolder}//src//output//index.js", - "request": "launch", - "smartStep": true, - "sourceMaps": true, - "type": "node" - } - ], - "version": "3.0.1" -} -``` - -`tasks.json` - -```json -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "3.0.1", - "tasks": [ - { - "type": "typescript", - "tsconfig": "src\\tsconfig.json", - "option": "watch", - "problemMatcher": [ - "$tsc-watch" - ], - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} -``` - -Then run the build step by pressing `Ctrl+Shift+B`, and any updates will trigger a TypeScript build. Debug by pressing `F5`. \ No newline at end of file From b7cd6c6e3353627a8664d118ca1daeb0a2be53dd Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 30 May 2020 10:55:16 -0700 Subject: [PATCH 36/39] Resolve conflicts with dev --- .gitignore | 5 ++--- .vscode/tasks.json | 19 ------------------- package.json | 2 +- yarn.lock | 7 ++++++- 4 files changed, 9 insertions(+), 24 deletions(-) delete mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 124fc50..27a5727 100644 --- a/.gitignore +++ b/.gitignore @@ -119,11 +119,10 @@ dist .vscode-test # yarn v2 - .yarn/cache .yarn/unplugged .yarn/build-state.yml .pnp.* -launch.json -tasks.json +# VSCode configuration files +.vscode/* diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index a327420..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "3.0.1", - "tasks": [ - { - "type": "typescript", - "tsconfig": "src\\tsconfig.json", - "option": "watch", - "problemMatcher": [ - "$tsc-watch" - ], - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} diff --git a/package.json b/package.json index bc20198..8f7e461 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@types/node": "^13.11.1", "add": "^2.0.6", "keybase-bot": "^3.6.1", - "moment": "^2.24.0", + "moment": "^2.26.0", "moment-timezone": "^0.5.28" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index c2203aa..50fe00c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,11 +70,16 @@ moment-timezone@^0.5.28: dependencies: moment ">= 2.9.0" -"moment@>= 2.9.0", moment@>=2.14.0, moment@^2.24.0: +"moment@>= 2.9.0", moment@>=2.14.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@^2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" + integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== + typescript@^3.9.2: version "3.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" From ee7c906bc40702192c7566fe1ff5db02fda4eee1 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 6 Jun 2020 12:45:16 -0700 Subject: [PATCH 37/39] Add validation of correct Node version --- package.json | 11 +++++++++-- src/scripts/check_node_version.ts | 7 +++++++ tsconfig.json | 3 ++- yarn.lock | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/scripts/check_node_version.ts diff --git a/package.json b/package.json index 8f7e461..5a4c6c7 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,24 @@ { "scripts": { - "build": "tsc" + "build": "tsc", + "requirements-check": "tsc src/scripts/check_node_version.ts" }, "dependencies": { "@types/moment-timezone": "^0.5.13", "@types/node": "^13.11.1", + "@types/semver": "^7.2.0", "add": "^2.0.6", "keybase-bot": "^3.6.1", "moment": "^2.26.0", - "moment-timezone": "^0.5.28" + "moment-timezone": "^0.5.28", + "semver": "^7.3.2" }, "devDependencies": { "typescript": "^3.9.2" }, + "engineStrict": true, + "engines": { + "node": "10.16.3" + }, "license": "MIT" } diff --git a/src/scripts/check_node_version.ts b/src/scripts/check_node_version.ts new file mode 100644 index 0000000..adbea91 --- /dev/null +++ b/src/scripts/check_node_version.ts @@ -0,0 +1,7 @@ +// @ts-ignore ts(6059): we want to import from package.json outside /src +import { engines } from '../../package.json' +import semver from 'semver' +const version = engines.node; +if (!semver.satisfies(process.version, version)) { + throw new Error(`The current node version${process.version} does not satisfy the required version ${version} .`); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b440529..ded43f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,7 +36,8 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "resolveJsonModule": true, // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ diff --git a/yarn.lock b/yarn.lock index 50fe00c..9da6b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,11 +9,23 @@ dependencies: moment ">=2.14.0" +"@types/node@*": + version "14.0.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3" + integrity sha512-lCvvI24L21ZVeIiyIUHZ5Oflv1hhHQ5E1S25IRlKIXaRkVgmXpJMI3wUJkmym2bTbCe+WoIibQnMVAU3FguaOg== + "@types/node@^13.11.1": version "13.11.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== +"@types/semver@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" + integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ== + dependencies: + "@types/node" "*" + add@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" @@ -80,6 +92,11 @@ moment@^2.26.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + typescript@^3.9.2: version "3.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" From 57d572016288009604ef16675004939e30297c85 Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 6 Jun 2020 13:30:42 -0700 Subject: [PATCH 38/39] Load environment variables from .env file using dotenv --- README.md | 24 +++++++++++++++++------- package.json | 1 + src/index.ts | 3 +++ tsconfig.json | 2 +- yarn.lock | 5 +++++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 807ec7b..54b807b 100644 --- a/README.md +++ b/README.md @@ -73,15 +73,25 @@ Bot: Removed availability of 0% for 3/25/2020 3/27/2020 America/New_York ## Running Locally -1. Use the proper version of Node & NPM -```base -nvm use $(cat .nvmrc) +1. Create a .env file in the root of the src/ folder, using .env.example as a base and replacing the values of +KEYBASE_AGENTAVAILABILITYBOT_USERNAME, KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY, KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME as appropriate. +2. Use the proper version of Node & NPM +```bash +nvm use +``` +3. Install dependencies +```bash +yarn install +``` +4. Compile TypeScript into JavaScript +```bash +cd src +npx tsc --project ../tsconfig.json ``` -2. Install dependencies +5. Run compiled JavaScript on Node ```bash -yarn +node output/src/index.js ``` -3. ## Debugging With VSCode @@ -94,7 +104,7 @@ Add this file to your `.vscode` folder at the root of the Git repository to debu "configurations": [ { "name": "Launch Program", - "program": "${workspaceFolder}//src//output//index.js", + "program": "${workspaceFolder}/src/output/src/index.js", "request": "launch", "smartStep": true, "sourceMaps": true, diff --git a/package.json b/package.json index 5a4c6c7..105c3aa 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@types/node": "^13.11.1", "@types/semver": "^7.2.0", "add": "^2.0.6", + "dotenv": "^8.2.0", "keybase-bot": "^3.6.1", "moment": "^2.26.0", "moment-timezone": "^0.5.28", diff --git a/src/index.ts b/src/index.ts index 3ff8a97..febe0bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,7 @@ import AgentAvailabilityBot from './bot/AgentAvailabilityBot' +import dotenv from 'dotenv' + +dotenv.config(); async function main() { const agentAvailabilityBot = new AgentAvailabilityBot(); diff --git a/tsconfig.json b/tsconfig.json index ded43f2..becd53e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./output", /* Redirect output structure to the directory. */ + "outDir": "./src/output", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "removeComments": true, /* Do not emit comments to output. */ diff --git a/yarn.lock b/yarn.lock index 9da6b4c..9289f1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,11 @@ add@^2.0.6: resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235" integrity sha1-JI8Kn25aUo7yKV2+7DBTITCuIjU= +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + isexe@2.0.0, isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" From 59d04ace092d328d0ebd1fcaf070bfe53a7c7e6e Mon Sep 17 00:00:00 2001 From: Ben Walker Date: Sat, 25 Jul 2020 16:10:43 -0700 Subject: [PATCH 39/39] Add Docker support --- Dockerfile | 32 ++++++++++++++++++++++++++++++++ README.md | 10 ++++++++++ package.json | 11 ++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..08c0f06 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM keybaseio/client:nightly-node + +## Install NVM +RUN rm /bin/sh && ln -s /bin/bash /bin/sh +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections +RUN apt-get update && apt-get install -y -q --no-install-recommends \ + apt-transport-https \ + build-essential \ + ca-certificates \ + curl \ + git \ + libssl-dev \ + wget \ + && rm -rf /var/lib/apt/lists/* +ENV NVM_DIR /root/.nvm +ENV NODE_VERSION 10.16.3 +RUN curl https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh| bash \ + && . $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/v$NODE_VERSION/bin:$PATH + +## Setup Keybase bot +RUN mkdir /app && chown keybase:keybase /app +WORKDIR /app +COPY . . +COPY src/.env src/.env +RUN yarn install --production=false +RUN npx tsc --project tsconfig.json +CMD node src/output/src/index.js \ No newline at end of file diff --git a/README.md b/README.md index 54b807b..d0edfec 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,16 @@ npx tsc --project ../tsconfig.json node output/src/index.js ``` +## Running Locally via Docker + +1. Create a .env file in the root of the src/ folder, using .env.example as a base and replacing the values of +KEYBASE_AGENTAVAILABILITYBOT_USERNAME, KEYBASE_AGENTAVAILABILITYBOT_PAPERKEY, KEYBASE_AGENTAVAILABILITYBOT_TEAMNAME as appropriate. +2. Run the following commands in the root of the repository: +```bash +docker build -t "keybase-docker-local" . +sudo docker run --env-file src/.env --rm keybase-docker-local +``` + ## Debugging With VSCode Add this file to your `.vscode` folder at the root of the Git repository to debug within Visual Studio Code: diff --git a/package.json b/package.json index 105c3aa..7bafaa3 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,13 @@ { + "name": "agent-availability", + "version": "0.0.1", + "main": "/src/output/src/index.js", + "description": "A Keybase bot used to signal dOrg Agent availability.", + "author": { + "email": "ben@dorg.tech", + "name": "Ben Walker", + "url": "https://dorg.tech" + }, "scripts": { "build": "tsc", "requirements-check": "tsc src/scripts/check_node_version.ts" @@ -22,4 +31,4 @@ "node": "10.16.3" }, "license": "MIT" -} +} \ No newline at end of file