From 5876183d9325d43b58b703ac1ffe538bea0ce549 Mon Sep 17 00:00:00 2001 From: Sharon Kennedy Date: Mon, 2 Mar 2020 17:52:25 -0500 Subject: [PATCH] use dotenv instead of config, sync with changes from glitch --- .env.sample | 7 ++ .gitignore | 2 +- package.json | 4 +- src/index.js | 301 ++++++++++++++++++++++++-------------------------- src/logger.js | 22 ++-- yarn.lock | 19 +--- 6 files changed, 172 insertions(+), 183 deletions(-) create mode 100644 .env.sample diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..23b840b --- /dev/null +++ b/.env.sample @@ -0,0 +1,7 @@ +MATRIX_SERVER_URL= +BOT_DISPLAY_NAME= +BOT_USERNAME= +BOT_PASSWORD= +BOT_USERID= +FACILITATOR_ROOM_ID= +CHAT_OFFLINE_MESSAGE= diff --git a/.gitignore b/.gitignore index d4752a1..4c54642 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -config +.env node_modules *.log \ No newline at end of file diff --git a/package.json b/package.json index 915eea7..540f0c4 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "Chatbot to manage interactions on OCRCC client chatbots", "main": "dist/ocrcc-chatbot.js", "scripts": { - "start": "nodemon --exec babel-node src/index.js" + "start": "nodemon -r dotenv/config --exec babel-node src/index.js" }, "author": "", "license": "ISC", "dependencies": { - "config": "^3.2.5", + "dotenv": "^8.2.0", "matrix-bot-sdk": "^0.5.0", "matrix-js-sdk": "^5.0.0", "node-localstorage": "^2.1.5", diff --git a/src/index.js b/src/index.js index 0c8421b..61dfaaa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,205 +1,194 @@ -import * as fs from 'fs' -import * as os from 'os' -import * as path from 'path' -import * as util from 'util' +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as util from "util"; import { LocalStorage } from "node-localstorage"; -import { uuid } from "uuidv4" -import config from 'config'; -global.Olm = require('olm'); +global.Olm = require("olm"); import * as matrix from "matrix-js-sdk"; -import logger from './logger' +import logger from "./logger"; -const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" }; + +const ENCRYPTION_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" }; class OcrccBot { constructor() { - this.awaitingAgreement = {} - this.awaitingFacilitator = {} - this.client = matrix.createClient(config.get('homeserverUrl')) + this.awaitingAgreement = {}; + this.awaitingFacilitator = {}; + this.client = matrix.createClient(process.env.MATRIX_SERVER_URL); + this.joinedRooms = []; } createLocalStorage() { - const storageLoc = `matrix-chatbot-${config.get('username')}` - const dir = path.resolve(path.join(os.homedir(), ".local-storage")) + const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`; + const dir = path.resolve(path.join(os.homedir(), ".local-storage")); if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } - const localStoragePath = path.resolve(path.join(dir, storageLoc)) + const localStoragePath = path.resolve(path.join(dir, storageLoc)); return new LocalStorage(localStoragePath); } sendMessage(roomId, msgText) { - return this.client.sendTextMessage(roomId, msgText) - .then((res) => { - logger.log('info', "Message sent") - logger.log('info', res) + return this.client + .sendTextMessage(roomId, msgText) + .then(res => { + logger.log("info", "Message sent"); + logger.log("info", res); }) - .catch((err) => { + .catch(err => { switch (err["name"]) { case "UnknownDeviceError": - Object.keys(err.devices).forEach((userId) => { - Object.keys(err.devices[userId]).map((deviceId) => { - this.client.setDeviceVerified(userId, deviceId, true); + Object.keys(err.devices).forEach(userId => { + Object.keys(err.devices[userId]).map(deviceId => { + this.client.setDeviceVerified(userId, deviceId, true); }); }); - return this.sendMessage(roomId, msgText) + return this.sendMessage(roomId, msgText); break; default: - logger.log('error', "Error sending message"); - logger.log('error', err); + logger.log("error", "Error sending message"); + logger.log("error", err); break; } - }) - } - - sendHtmlMessage(roomId, msgText, msgHtml) { - return this.client.sendHtmlMessage(roomId, msgText, msgHtml) - .then((res) => { - logger.log('info', "Message sent") - logger.log('info', res) - }) - .catch((err) => { - switch (err["name"]) { - case "UnknownDeviceError": - Object.keys(err.devices).forEach((userId) => { - Object.keys(err.devices[userId]).map((deviceId) => { - this.client.setDeviceVerified(userId, deviceId, true); - }); - }); - return this.sendHtmlMessage(roomId, msgText, msgHtml) - break; - default: - logger.log('error', "Error sending message"); - logger.log('error', err); - break; - } - }) + }); } inviteFacilitators(roomId) { - this.awaitingFacilitator[roomId] = true - this.client.getJoinedRoomMembers(config.get('waitingRoomId')) - .then((members) => { - Object.keys(members["joined"]).forEach((member) => { - if (member !== config.get('userId')) - this.client.invite(roomId, member) + this.awaitingFacilitator[roomId] = true; + let chatOffline = true; + this.client + .getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID) + .then(members => { + let onlineMembersCount = 0; + Object.keys(members["joined"]).forEach(member => { + const user = this.client.getUser(member); + if (user.presence === "online" && member !== process.env.BOT_USERID) { + logger.log("info", "INVITING MEMBER: " + member); + chatOffline = false; + this.client.invite(roomId, member); + } + }); }) - }) - // const notif = `There is a support seeker waiting. Go to https://riot.im/app/#/room/${roomId} to respond.` - // sendMessage(waitingRoomId, notif) + .then(() => { + if (chatOffline) { + this.sendMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE); + } + }) + .catch(err => { + logger.log("error", "ERROR GETTING ROOM MEMBERS"); + logger.log("error", err); + }); } uninviteFacilitators(roomId) { - this.awaitingFacilitator[roomId] = false - this.client.getJoinedRoomMembers(config.get('waitingRoomId')) - .then((allFacilitators) => { - this.client.getJoinedRoomMembers(roomId) - .then((roomMembers) => { - const membersIds = Object.keys(roomMembers["joined"]) - const facilitatorsIds = Object.keys(allFacilitators["joined"]) - facilitatorsIds.forEach((f) => { - if (!membersIds.includes(f)) { - logger.log("info", "kicking out " + f + " from " + roomId) - this.client.kick(roomId, f, "A facilitator has already joined this chat.") - .then(() => { - logger.log("info", "Kick success") - }) - .catch((err) => { - logger.log("error", err) - }) - } - }) + this.awaitingFacilitator[roomId] = false; + this.client + .getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID) + .then(allFacilitators => { + this.client.getJoinedRoomMembers(roomId).then(roomMembers => { + const membersIds = Object.keys(roomMembers["joined"]); + const facilitatorsIds = Object.keys(allFacilitators["joined"]); + facilitatorsIds.forEach(f => { + if (!membersIds.includes(f)) { + logger.log("info", "kicking out " + f + " from " + roomId); + this.client + .kick(roomId, f, "A facilitator has already joined this chat.") + .then(() => { + logger.log("info", "Kick success"); + }) + .catch(err => { + logger.log("error", err); + }); + } + }); + }); }) - }) + .catch(err => logger.log("error", err)); } start() { - const localStorage = this.createLocalStorage() - let deviceId = localStorage.getItem('deviceId') + const localStorage = this.createLocalStorage(); - this.client.login('m.login.password', { - user: config.get('username'), - password: config.get('password'), - initial_device_display_name: config.get('botName'), - deviceId: deviceId, - }) - .then((data) => { - const accessToken = data.access_token - const deviceId = data.device_id + this.client + .login("m.login.password", { + user: process.env.BOT_USERNAME, + password: process.env.BOT_PASSWORD, + initial_device_display_name: process.env.BOT_DISPLAY_NAME + }) + .then(data => { + const accessToken = data.access_token; + const deviceId = data.device_id; - localStorage.setItem('deviceId', data.device_id) + // create new client with full options - // create new client with full options + let opts = { + baseUrl: process.env.MATRIX_SERVER_URL, + accessToken: accessToken, + userId: process.env.BOT_USERID, + deviceId: deviceId, + sessionStore: new matrix.WebStorageSessionStore(localStorage) + }; - let opts = { - baseUrl: config.get('homeserverUrl'), - accessToken: accessToken, - userId: config.get('userId'), - deviceId: deviceId, - sessionStore: new matrix.WebStorageSessionStore(localStorage), - } - - this.client = matrix.createClient(opts) - }) - .catch(err => { - logger.log('error', `Login error: ${err}`) - }) - .then(() => this.client.initCrypto()) - .then(() => { - - // Automatically accept all room invitations - // On joining a room, send the intro messages and wait for agreement to continue - this.client.on("RoomMember.membership", (event, member) => { - if (member.membership === "invite" && member.userId === config.get('userId')) { - logger.log("info", "Auto-joining room " + member.roomId) - this.client.joinRoom(member.roomId) - .then(() => this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG)) - .then(() => { - if (member.roomId !== config.get('waitingRoomId')) { - this.sendMessage(member.roomId, config.get('introMessage')) - .then(() => this.sendHtmlMessage(member.roomId, `Please read the terms and conditions at ${config.get('termsUrl')}`, `Please read the full terms and conditions.`)) - .then(() => this.sendMessage(member.roomId, config.get('agreementMessage'))) - .then(() => this.awaitingAgreement[member.roomId] = true) - } - }) - } - - // When the first facilitator joins a support session, uninvite the other facilitators - if (member.membership === 'join' && this.awaitingFacilitator[member.roomId]) { - this.uninviteFacilitators(member.roomId) - } - }); - - // Listen for incoming messages - this.client.on('Event.decrypted', (event) => { - if (event.getType() === 'm.room.message') { - const roomId = event.getRoomId() - const sender = event.getSender() - const content = event.getContent() - const body = content.body - - // Listen for the user to agree to continue, then invite facilitators to join - if (sender !== config.get('userId') && this.awaitingAgreement[roomId]) { - if (body.toLowerCase().startsWith('yes')) { - this.sendMessage(roomId, config.get('confirmationMessage')) - this.inviteFacilitators(roomId) - this.awaitingAgreement[roomId] = false - } else { - this.sendMessage(roomId, config.get('exitMessage')) - this.awaitingAgreement[roomId] = false - } + this.client = matrix.createClient(opts); + }) + .catch(err => { + logger.log("error", `Login error: ${err}`); + }) + .then(() => + this.client.initCrypto().catch(err => { + logger.log("error", `ERROR STARTING CRYPTO: ${err}`); + }) + ) + .then(() => + this.client.getJoinedRooms().then(data => { + this.joinedRooms = data["joined_rooms"]; + }) + ) + .then(() => { + // Automatically accept all room invitations + this.client.on("RoomMember.membership", (event, member) => { + if ( + member.membership === "invite" && + member.userId === process.env.BOT_USERID && + !this.joinedRooms.includes(member.roomId) + ) { + logger.log("info", "Auto-joining room " + member.roomId); + this.client + .joinRoom(member.roomId) + .then(room => { + this.sendMessage( + process.env.FACILITATOR_ROOM_ID, + `A support seeker requested a chat (Room ID: ${member.roomId})` + ); + this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG); + }) + .then(() => this.inviteFacilitators(member.roomId)); } - } - }); - }) - .finally(() => this.client.startClient()) + // When the first facilitator joins a support session, uninvite the other facilitators + if ( + member.membership === "join" && + member.userId !== process.env.BOT_USERID && + this.awaitingFacilitator[member.roomId] + ) { + this.sendMessage( + member.roomId, + `${member.name} has joined the chat.` + ); + this.sendMessage( + process.env.FACILITATOR_ROOM_ID, + `${member.name} joined the chat (Room ID: ${member.roomId})` + ); + this.uninviteFacilitators(member.roomId); + } + }); + }) + .finally(() => this.client.startClient()); } - } const bot = new OcrccBot(); -bot.start() +bot.start(); diff --git a/src/logger.js b/src/logger.js index ac2b30b..667fa96 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,16 +1,16 @@ -import winston from 'winston' +import winston from "winston"; const logger = winston.createLogger({ - level: 'info', + level: "info", format: winston.format.json(), - defaultMeta: { service: 'user-service' }, + defaultMeta: { service: "user-service" }, transports: [ // // - Write all logs with level `error` and below to `error.log` // - Write all logs with level `info` and below to `combined.log` // - new winston.transports.File({ filename: 'error.log', level: 'error' }), - new winston.transports.File({ filename: 'combined.log' }) + new winston.transports.File({ filename: "error.log", level: "error" }), + new winston.transports.File({ filename: "combined.log" }) ] }); @@ -18,10 +18,12 @@ const logger = winston.createLogger({ // If we're not in production then log to the `console` with the format: // `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` // -if (process.env.NODE_ENV !== 'production') { - logger.add(new winston.transports.Console({ - format: winston.format.simple() - })); +if (process.env.NODE_ENV !== "production") { + logger.add( + new winston.transports.Console({ + format: winston.format.simple() + }) + ); } -export default logger; \ No newline at end of file +export default logger; diff --git a/yarn.lock b/yarn.lock index 65504ac..2b26cfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1167,13 +1167,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -config@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/config/-/config-3.2.5.tgz#ab10ab88b61a873fbf9a5f0c6b4a22750422f243" - integrity sha512-8itpjyR01lAJanhAlPncBngYRZez/LoRLW8wnGi+6SEcsUyA1wvHvbpIrAJYDJT+W9BScnj4mYoUgbtp9I+0+Q== - dependencies: - json5 "^1.0.1" - configstore@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" @@ -1358,6 +1351,11 @@ dot-prop@^4.1.0: dependencies: is-obj "^1.0.0" +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== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -1983,13 +1981,6 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - json5@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"