From 7b2da6b351e5a512f4953a47838599c7efe94233 Mon Sep 17 00:00:00 2001 From: Sharon Kennedy Date: Sun, 15 Mar 2020 14:10:59 -0400 Subject: [PATCH] changes from glitch --- package.json | 10 +- src/index.js | 269 ++++++++++++++++++++++++++++++++++++++-------- transcripts/.keep | 0 3 files changed, 227 insertions(+), 52 deletions(-) create mode 100644 transcripts/.keep diff --git a/package.json b/package.json index 540f0c4..9f48a73 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,14 @@ "description": "Chatbot to manage interactions on OCRCC client chatbots", "main": "dist/ocrcc-chatbot.js", "scripts": { - "start": "nodemon -r dotenv/config --exec babel-node src/index.js" + "develop": "nodemon --exec babel-node src/index.js", + "build": "babel src -d dist", + "start": "yarn build && node dist/index.js" }, "author": "", "license": "ISC", "dependencies": { - "dotenv": "^8.2.0", - "matrix-bot-sdk": "^0.5.0", - "matrix-js-sdk": "^5.0.0", + "matrix-js-sdk": "^5.0.1", "node-localstorage": "^2.1.5", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", "uuidv4": "^6.0.2", @@ -24,4 +24,4 @@ "babel-plugin-dynamic-import-node": "^2.3.0", "nodemon": "^2.0.2" } -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 61dfaaa..489a735 100644 --- a/src/index.js +++ b/src/index.js @@ -10,15 +10,18 @@ import * as matrix from "matrix-js-sdk"; import logger from "./logger"; - const ENCRYPTION_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" }; +const KICK_REASON = "A facilitator has already joined this chat."; +const BOT_ERROR_MESSAGE = + "Something went wrong on our end, please restart the chat and try again."; +const MAX_RETRIES = 3; class OcrccBot { constructor() { - this.awaitingAgreement = {}; this.awaitingFacilitator = {}; this.client = matrix.createClient(process.env.MATRIX_SERVER_URL); this.joinedRooms = []; + this.activeChatrooms = {}; } createLocalStorage() { @@ -32,28 +35,86 @@ class OcrccBot { } sendMessage(roomId, msgText) { - return this.client - .sendTextMessage(roomId, msgText) - .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.client.sendTextMessage(roomId, msgText).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.sendMessage(roomId, msgText); - break; - default: - logger.log("error", "Error sending message"); - logger.log("error", err); - break; - } - }); + }); + return this.sendMessage(roomId, msgText); + break; + default: + logger.log("error", `ERROR SENDING MESSAGE: ${err}`); + this.handleBotCrash(roomId, err); + break; + } + }); + } + + inviteUserToRoom(client, roomId, member, retries = 0) { + logger.log("info", "INVITING MEMBER: " + member); + if (retries > MAX_RETRIES) { + this.handleBotCrash(roomId, "Rate limit exceeded for bot account"); + return logger.log( + "error", + `RATE LIMIT EXCEEDED AND RETRY LIMIT EXCEEDED` + ); + } + return client.invite(roomId, member).catch(err => { + switch (err["name"]) { + case "M_LIMIT_EXCEEDED": + logger.log("info", "Rate limit exceeded, retrying."); + const retryCount = retries + 1; + const delay = retryCount * 2 * 1000; + return setTimeout( + this.inviteUserToRoom, + delay, + client, + roomId, + member, + retryCount + ); + break; + default: + logger.log("error", `ERROR INVITING MEMBER: ${err}`); + this.handleBotCrash(roomId, err); + break; + } + }); + } + + kickUserFromRoom(client, roomId, member, retries = 0) { + logger.log("info", "KICKING OUT MEMBER: " + member); + if (retries > MAX_RETRIES) { + this.handleBotCrash(roomId, "Rate limit exceeded for bot account."); + return logger.log( + "error", + `RATE LIMIT EXCEEDED AND RETRY LIMIT EXCEEDED` + ); + } + return client.kick(roomId, member, KICK_REASON).catch(err => { + switch (err["name"]) { + case "M_LIMIT_EXCEEDED": + logger.log("info", "Rate limit exceeded, retrying."); + const retryCount = retries + 1; + const delay = retryCount * 2 * 1000; + return setTimeout( + this.kickUserFromRoom, + delay, + client, + roomId, + member, + retryCount + ); + break; + default: + this.handleBotCrash(roomId, err); + logger.log("error", `ERROR KICKING OUT MEMBER: ${err}`); + break; + } + }); } inviteFacilitators(roomId) { @@ -66,9 +127,8 @@ class OcrccBot { 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); + this.inviteUserToRoom(this.client, roomId, member); } }); }) @@ -78,8 +138,8 @@ class OcrccBot { } }) .catch(err => { - logger.log("error", "ERROR GETTING ROOM MEMBERS"); - logger.log("error", err); + this.handleBotCrash(roomId, err); + logger.log("error", `ERROR GETTING ROOM MEMBERS: ${err}`); }); } @@ -93,20 +153,49 @@ class OcrccBot { 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.kickUserFromRoom(this.client, roomId, f); } }); }); }) - .catch(err => logger.log("error", err)); + .catch(err => { + this.handleBotCrash(roomId, err); + logger.log("error", err); + }); + } + + handleBotCrash(roomId, error) { + if (roomId) { + this.sendMessage(roomId, BOT_ERROR_MESSAGE); + } + + this.sendMessage( + process.env.FACILITATOR_ROOM_ID, + `The Help Bot ran into an error: ${error}. Please verify that the chat service is working.` + ); + } + + writeToTranscript(event) { + try { + const sender = event.getSender(); + const roomId = event.getRoomId(); + const content = event.getContent(); + const date = event.getDate(); + const time = date.toLocaleTimeString("en-GB", { + timeZone: "America/New_York" + }); + const filepath = this.activeChatrooms[roomId].transcriptFile; + + if (!content) { + return; + } + + const message = `${sender} [${time}]: ${content.body}\n`; + + fs.appendFileSync(filepath, message, "utf8"); + } catch (err) { + logger.log("error", `ERROR APPENDING TO TRANSCRIPT FILE: ${err}`); + } } start() { @@ -135,13 +224,36 @@ class OcrccBot { this.client = matrix.createClient(opts); }) .catch(err => { - logger.log("error", `Login error: ${err}`); + logger.log("error", `ERROR WITH LOGIN: ${err}`); }) - .then(() => - this.client.initCrypto().catch(err => { - logger.log("error", `ERROR STARTING CRYPTO: ${err}`); - }) - ) + .then(() => { + this.client.getDevices().then(data => { + const currentDeviceId = this.client.getDeviceId(); + const allDeviceIds = data.devices.map(d => d.device_id); + const oldDevices = allDeviceIds.filter(id => id !== currentDeviceId); + logger.log("info", `DELETING OLD DEVICES: ${oldDevices}`); + this.client.deleteMultipleDevices(oldDevices).catch(err => { + const auth = { + session: err.data.session, + type: "m.login.password", + user: process.env.BOT_USERID, + identifier: { type: "m.id.user", user: process.env.BOT_USERID }, + password: process.env.BOT_PASSWORD + }; + this.client + .deleteMultipleDevices(oldDevices, auth) + .then(() => logger.log("info", "DELETED OLD DEVICES")) + .catch(err => + logger.log( + "error", + `ERROR DELETING OLD DEVICES: ${JSON.stringify(err.data)}` + ) + ); + }); + }); + }) + .then(() => this.client.initCrypto()) + .catch(err => logger.log("error", `ERROR STARTING CRYPTO: ${err}`)) .then(() => this.client.getJoinedRooms().then(data => { this.joinedRooms = data["joined_rooms"]; @@ -163,17 +275,22 @@ class OcrccBot { 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)); + .then(() => this.inviteFacilitators(member.roomId)) + .catch(err => { + logger.log("error", err); + }); } - // When the first facilitator joins a support session, uninvite the other facilitators + // When a facilitator joins a support session, revoke the other invitations if ( member.membership === "join" && member.userId !== process.env.BOT_USERID && this.awaitingFacilitator[member.roomId] ) { + this.activeChatrooms[member.roomId] = { + facilitator: member.userId + }; this.sendMessage( member.roomId, `${member.name} has joined the chat.` @@ -183,10 +300,68 @@ class OcrccBot { `${member.name} joined the chat (Room ID: ${member.roomId})` ); this.uninviteFacilitators(member.roomId); + if (process.env.CAPTURE_TRANSCRIPTS) { + const currentDate = new Date(); + const dateOpts = { + year: "numeric", + month: "short", + day: "numeric" + }; + const chatDate = currentDate.toLocaleDateString( + "en-GB", + dateOpts + ); + const chatTime = currentDate.toLocaleTimeString("en-GB", { + timeZone: "America/New_York" + }); + const filename = `${chatDate} - ${chatTime} - ${member.roomId}.txt`; + const filepath = path.resolve(path.join("transcripts", filename)); + this.activeChatrooms[member.roomId].transcriptFile = filepath; + } + } + + if ( + member.membership === "leave" && + member.userId !== process.env.BOT_USERID && + this.activeChatrooms[member.roomId] && + member.userId === this.activeChatrooms[member.roomId].facilitator + ) { + this.sendMessage( + member.roomId, + `${member.name} has left the chat.` + ); } }); + + if (process.env.CAPTURE_TRANSCRIPTS) { + // encrypted messages + this.client.on("Event.decrypted", (event, err) => { + if (err) { + return logger.log("error", `ERROR DECRYPTING EVENT: ${err}`); + } + if (event.getType() === "m.room.message") { + this.writeToTranscript(event); + } + }); + // unencrypted messages + this.client.on("Room.timeline", (event, room, toStartOfTimeline) => { + if ( + event.getType() === "m.room.message" && + !this.client.isCryptoEnabled() + ) { + if (event.isEncrypted()) { + return; + } + this.writeToTranscript(event); + } + }); + } }) - .finally(() => this.client.startClient()); + .then(() => this.client.startClient({ initialSyncLimit: 0 })) + .catch(err => { + this.handleBotCrash(undefined, err); + logger.log("error", `ERROR INITIALIZING CLIENT: ${err}`); + }); } } diff --git a/transcripts/.keep b/transcripts/.keep new file mode 100644 index 0000000..e69de29