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 51c5b10..27a5727 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ +_temp/ +.DS_Store +gulp-tsc-tmp-* +.gulp-tsc-tmp-* +output/ +*.js +*.js.map +*.d.ts +config.json +!gulpfile.js + # Logs logs *.log @@ -108,8 +119,10 @@ dist .vscode-test # yarn v2 - .yarn/cache .yarn/unplugged .yarn/build-state.yml .pnp.* + +# VSCode configuration files +.vscode/* diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..70047db --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v10.16.3 diff --git a/Dockerfile b/Dockerfile index a9cedb9..08c0f06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +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 . /app -RUN yarn install -CMD node /app/index.js +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 0d6e263..d0edfec 100644 --- a/README.md +++ b/README.md @@ -1 +1,171 @@ # AgentAvailability + +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 +- [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 +``` + +``` +User: /avail get userb +Bot: Availability for user userb: +Default: 50% +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% +- [5/02/2020 - 5/10/2020] 75% +``` + +``` +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 +``` + +``` +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 +``` + +``` +User: /avail rm +Bot: Which availability would you like to remove? +Default: 50% +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 America/New_York +``` + +## Running Locally + +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 +``` +5. Run compiled JavaScript on Node +```bash +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: + +`launch.json` ([documentation](https://go.microsoft.com/fwlink/?linkid=830387)) + +```json +{ + "configurations": [ + { + "name": "Launch Program", + "program": "${workspaceFolder}/src/output/src/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 +* Setup test users to test multiple users functionality +* 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" +* 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 +``` +* 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) diff --git a/index.js b/index.js deleted file mode 100644 index 22d61f1..0000000 --- a/index.js +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env node -const Bot = require('keybase-bot') -const mathjs = require('mathjs') - -// -// 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!` -// - -const bot = new Bot() - -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 -} - -function main() { - const username = process.env.KB_USERNAME - const paperkey = process.env.KB_PAPERKEY - 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 '/math '`) - const onMessage = message => { - if (message.content.type === 'text') { - const prefix = message.content.text.body.slice(0, 6) - if (prefix === '/math ') { - const reply = {body: msgReply(message.content.text.body.slice(6))} - bot.chat.send(message.conversationId, reply) - } - } - } - const onError = e => console.error(e) - bot.chat.watchAllChannelsForNewMessages(onMessage, onError) - }) - .catch(error => { - console.error(error) - shutDown() - }) -} - -function shutDown() { - bot.deinit().then(() => process.exit()) -} - -process.on('SIGINT', shutDown) -process.on('SIGTERM', shutDown) - -main() diff --git a/package.json b/package.json index bb1b9b7..7bafaa3 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,34 @@ { - "name": "AgentAvailability", - "version": "1.0.0", - "main": "index.js", - "author": "", - "license": "MIT", - "private": true, + "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" + }, "dependencies": { + "@types/moment-timezone": "^0.5.13", + "@types/node": "^13.11.1", + "@types/semver": "^7.2.0", + "add": "^2.0.6", + "dotenv": "^8.2.0", "keybase-bot": "^3.6.1", - "mathjs": "^6.6.1" - } -} + "moment": "^2.26.0", + "moment-timezone": "^0.5.28", + "semver": "^7.3.2" + }, + "devDependencies": { + "typescript": "^3.9.2" + }, + "engineStrict": true, + "engines": { + "node": "10.16.3" + }, + "license": "MIT" +} \ No newline at end of file diff --git a/src/bot/AgentAvailabilityBot.ts b/src/bot/AgentAvailabilityBot.ts new file mode 100644 index 0000000..f598c0b --- /dev/null +++ b/src/bot/AgentAvailabilityBot.ts @@ -0,0 +1,306 @@ +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' + +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; } = { + 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: { [id: string]: string | undefined; } = { + default: process.env.KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_DEFAULT, + timezone: process.env.KEYBASE_AGENTAVAILABILITYBOT_CONFIGKEY_TIMEZONE, + }; + dateFormat: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_DATEFORMAT; + inputDateFormat: string | undefined = process.env.KEYBASE_AGENTAVAILABILITYBOT_INPUTDATEFORMAT; + momentTimezoneNames: string[] = momentTimezone.tz.names(); + 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.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(workLevelRegex?: RegExp) { + super(); + if (workLevelRegex) { + this.workLevelRegex = workLevelRegex; + } + } + + async initBot(): Promise { + try { + await this.init(this.username || '', this.paperkey || ''); + this._startUp(); + } + catch (error) { + console.error(error); + await this.deinit(); + } + } + + async deinitBot(): Promise { + console.log('Shutting down...'); + await this.deinit(); + return process.exit(); + } + + _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); + if (prefix === this.commandPrefix) { + const reply = { body: await this._msgReply(message) }; + this.chat.send(message.conversationId, reply); + } + } + } + this.chat.watchAllChannelsForNewMessages(onMessage, onError); + } + + _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; + } + + _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 { + let validatedDate: Moment = moment(date, this.dateFormat, true); + if (validatedDate.isValid()) { + return true; + } + return false; + } + + _isValidTimezone(timezone: string): boolean { + if (this.momentTimezoneNames.indexOf(timezone) > -1) { + return true; + } + return false; + } + + _isValidUsername(username: string): boolean { + return true; + } + + _isValidWorkLevel(worklevel: string): boolean { + if (this.workLevelRegex.test(worklevel)) { + return true + } + return false; + } + + _writeArgsErrorMessage(args: string[]): string { + let errorMessage: string = `Invalid arguments: ${args.toString()}`; + console.error(errorMessage); + return errorMessage; + } + + _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 { + let newAvailability: Availability = { + startDate: '', + endDate: '', + workLevel: '' + } + let timezone = (await this.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.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.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 { + if (args[0]) { + if (this._isValidUsername(args[0])) { + username = args[0] + } + else { + 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; + 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)}`; + } + + 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]); + 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]); + return `Your time zone has been updated to ${args[1]}`; + } + else { + return this._writeArgsErrorMessage(args); + } + } + + 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); + } + + if (args.length === 0) { + let availabilitiesString = (await this.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.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.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.kvstore.put(this.teamName, this.nameSpaces.availabilities || '', username, JSON.stringify(availabilities)); + if (availabilityToRemove) { + return `Removed availability ${this._getAvailabilityString(availabilityToRemove, timezone)}`; + } + } + return this._writeArgsErrorMessage(args); + } + + 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 || ''); + } + else if (args[1] === this.commandVerbs.get) { + args.splice(0, 2); + return this._getValues(args, message?.sender?.username || ''); + } + else if (args[1] === this.commandVerbs.set) { + args.splice(0, 2); + return this._setValue(args, message?.sender?.username || ''); + } + else if (args[1] === this.commandVerbs.rm) { + 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/bot/Availability.ts b/src/bot/Availability.ts new file mode 100644 index 0000000..eb20dfb --- /dev/null +++ b/src/bot/Availability.ts @@ -0,0 +1,6 @@ +export default interface Availability +{ + startDate: string; + endDate: string; + workLevel: string; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..febe0bc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,28 @@ +import AgentAvailabilityBot from './bot/AgentAvailabilityBot' +import dotenv from 'dotenv' + +dotenv.config(); + +async function main() { + const agentAvailabilityBot = new AgentAvailabilityBot(); + await agentAvailabilityBot.initBot(); + 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); + }); +} + +export { + AgentAvailabilityBot +} 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 new file mode 100644 index 0000000..becd53e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,60 @@ +{ + "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": "./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. */ + // "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). */ + "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. */ + // "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/yarn.lock index f2b6926..9289f1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,36 +2,45 @@ # 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== - -decimal.js@^10.2.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== +"@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@*": + 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" + 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" 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 +68,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,20 +80,32 @@ 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== +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: + 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== + +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" + integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== which@1.3.1: version "1.3.1"