diff --git a/.env.sample b/.env.sample index cd488d2..2538394 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,5 @@ BOT_DISPLAY_NAME= BOT_USERNAME= BOT_PASSWORD= BOT_USERID= -FACILITATOR_GROUP_ID= FACILITATOR_ROOM_ID= CAPTURE_TRANSCRIPTS \ No newline at end of file diff --git a/dist/bot.js b/dist/bot.js index a17c942..0086e19 100644 --- a/dist/bot.js +++ b/dist/bot.js @@ -28,7 +28,6 @@ global.Olm = require("olm"); class OcrccBot { constructor(botConfig) { this.config = botConfig; - this.awaitingFacilitator = {}; this.client = matrix.createClient(this.config.MATRIX_SERVER_URL); this.joinedRooms = []; } @@ -54,11 +53,38 @@ class OcrccBot { this.sendMessage(roomId, content); } - async sendMessage(roomId, content) { - _logger.default.log("info", `SENDING MESSAGE: ${content.body}`); + async sendNotice(roomId, message) { + _logger.default.log("info", `SENDING *NOTICE*: ${message}`); + try { + await this.client.sendNotice(roomId, message); + } catch (err) { + switch (err["name"]) { + case "UnknownDeviceError": + Object.keys(err.devices).forEach(userId => { + Object.keys(err.devices[userId]).map(async deviceId => { + try { + await this.client.setDeviceVerified(userId, deviceId, true); + } catch (err) { + _logger.default.log("error", `ERROR VERIFYING DEVICE: ${err}`); + } + }); + }); + await this.sendNotice(roomId, message); + + default: + _logger.default.log("error", `ERROR SENDING *NOTICE*: ${err}`); + + break; + } + } + } + + async sendMessage(roomId, content) { try { await this.client.sendMessage(roomId, content); + + _logger.default.log("info", `SENT MESSAGE: ${content.body}`); } catch (err) { switch (err["name"]) { case "UnknownDeviceError": @@ -101,25 +127,26 @@ class OcrccBot { async inviteFacilitators(roomId) { this.localStorage.setItem(`${roomId}-waiting`, 'true'); - let chatOffline = true; + let invitations = []; try { - const data = await this.client.getGroupUsers(this.config.FACILITATOR_GROUP_ID); - const members = data.chunk; - members.forEach(member => { - const memberId = member.user_id; + const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const members = Object.keys(roomMembers["joined"]); + members.forEach(memberId => { const user = this.client.getUser(memberId); - if (user && user.presence === "online" && memberId !== this.config.BOT_USERID) { - chatOffline = false; + if (user && user.presence !== "offline" && memberId !== this.config.BOT_USERID) { + invitations.push(memberId); this.inviteUserToRoom(roomId, memberId); } }); - if (chatOffline) { + if (invitations.length > 0) { + this.localStorage.setItem(`${roomId}-invitations`, invitations); + } else { _logger.default.log('info', "NO FACILITATORS ONLINE"); - this.sendTextMessage(roomId, this.config.CHAT_OFFLINE_MESSAGE); + this.sendNotice(roomId, "CHAT_OFFLINE"); } } catch (err) { this.handleBotCrash(roomId, err); @@ -132,12 +159,13 @@ class OcrccBot { this.localStorage.removeItem(`${roomId}-waiting`); try { - const groupUsers = await this.client.getGroupUsers(this.config.FACILITATOR_GROUP_ID); - const roomMembers = await this.client.getJoinedRoomMembers(roomId); - const membersIds = Object.keys(roomMembers["joined"]); - const facilitatorsIds = groupUsers.chunk.map(f => f.user_id); + const facilitatorsRoomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const supportRoomMembers = await this.client.getJoinedRoomMembers(roomId); + const roomMembersIds = Object.keys(supportRoomMembers["joined"]); + const facilitatorsIds = Object.keys(facilitatorsRoomMembers["joined"]); + if (!roomMembersIds || !facilitatorsIds) return; facilitatorsIds.forEach(f => { - if (!membersIds.includes(f)) { + if (!roomMembersIds.includes(f)) { this.kickUserFromRoom(roomId, f); } }); @@ -212,6 +240,10 @@ class OcrccBot { this.sendTranscript(senderId, roomId); break; + case "delete transcript": + this.deleteTranscript(senderId, roomId); + break; + case "hi": const responses = ["Hi!", "Hello", "Hey :)", "Hi there", "Bleep bloop"]; const message = responses[Math.floor(Math.random() * responses.length)]; @@ -258,16 +290,25 @@ class OcrccBot { } const filename = path.basename(transcriptFile) || "Transcript"; - const stream = fs.createReadStream(transcriptFile); - const contentUrl = await this.client.uploadContent({ - stream: stream, + const file = fs.readFileSync(transcriptFile); + const stats = fs.statSync(transcriptFile); + const url = await this.client.uploadContent(file, { + rawResponse: false, name: filename }); + + _logger.default.log('info', url); + const content = { msgtype: "m.file", body: filename, - url: JSON.parse(contentUrl).content_uri, - showToUser: senderId + info: { + size: stats.size, + mimetype: 'text/plain' + }, + url: url.content_uri, + showToUser: senderId, + mimetype: 'text/plain' }; this.sendMessage(roomId, content); } catch (err) { @@ -277,6 +318,30 @@ class OcrccBot { } } + deleteTranscript(senderId, roomId) { + const transcriptFile = this.localStorage.getItem(`${roomId}-transcript`); + + if (!transcriptFile) { + return this.sendTextMessage(roomId, "There is no transcript for this chat.", senderId); + } + + fs.unlink(transcriptFile, err => { + if (err) { + _logger.default.log('error', "UNABLE TO DELETE TRANSCRIPT FILE => " + transcriptFile); + + _logger.default.log('error', err); + + return this.sendTextMessage(roomId, `There was an error deleting the transcript: ${err}`, senderId); + } + + this.localStorage.removeItem(`${roomId}-transcript`); + + _logger.default.log('info', "DELETED TRANSCRIPT FILE => " + transcriptFile); + + this.sendTextMessage(roomId, `The transcript file has been deleted.`, senderId); + }); + } + async deleteOldDevices() { const currentDeviceId = this.client.getDeviceId(); const deviceData = await this.client.getDevices(); @@ -316,53 +381,76 @@ class OcrccBot { this.client.on("RoomMember.membership", async (event, member) => { if (member.membership === "invite" && member.userId === this.config.BOT_USERID && !this.joinedRooms.includes(member.roomId)) { try { - const room = await this.client.joinRoom(member.roomId); + const roomData = await this.client.getJoinedRooms(); + const joinedRooms = roomData["joined_rooms"]; - _logger.default.log("info", "AUTO JOINED ROOM => " + room.roomId); + if (!joinedRooms.includes(member.roomId)) { + const room = await this.client.joinRoom(member.roomId); - this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, `A support seeker requested a chat (Room ID: ${room.roomId})`); - this.inviteFacilitators(room.roomId); + _logger.default.log("info", "AUTO JOINED ROOM => " + room.roomId); + + const currentDate = new Date(); + const chatDate = currentDate.toLocaleDateString(); + const chatTime = currentDate.toLocaleTimeString(); + const roomId = room.roomId.split(':')[0]; + const notification = `Incoming support chat at ${chatTime} (room ID: ${roomId})`; + this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); + this.inviteFacilitators(room.roomId); + } } catch (err) { _logger.default.log("error", "ERROR JOINING ROOM => " + err); } - } // When a facilitator joins a support session, make them a moderator - // revoke the other invitations - + } if (member.membership === "join" && member.userId !== this.config.BOT_USERID && this.localStorage.getItem(`${member.roomId}-waiting`)) { - this.localStorage.setItem(`${member.roomId}-facilitator`, member.userId); - const event = { - getType: () => { - return "m.room.power_levels"; - }, - getContent: () => { - return { - users: { - [this.config.BOT_USERID]: 100, - [member.userId]: 50 - } - }; - } - }; - this.client.setPowerLevel(member.roomId, member.userId, 50, event); - this.sendTextMessage(member.roomId, `${member.name} has joined the chat.`); - this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, `${member.name} joined the chat (Room ID: ${member.roomId})`); - this.uninviteFacilitators(member.roomId); + // make sure it's a facilitator joining + const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const members = Object.keys(roomMembers["joined"]); + const isFacilitator = members.includes(member.userId); - if (this.config.CAPTURE_TRANSCRIPTS) { - const currentDate = new Date(); - const dateOpts = { - year: "numeric", - month: "short", - day: "numeric" + if (isFacilitator) { + // made facilitator a moderator in the room + this.localStorage.setItem(`${member.roomId}-facilitator`, member.userId); + const event = { + getType: () => { + return "m.room.power_levels"; + }, + getContent: () => { + return { + users: { + [this.config.BOT_USERID]: 100, + [member.userId]: 50 + } + }; + } }; - 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.localStorage.setItem(`${member.roomId}-transcript`, filepath); + this.client.setPowerLevel(member.roomId, member.userId, 50, event); // send notification to Support Chat Notifications room + + const currentDate = new Date(); + const chatTime = currentDate.toLocaleTimeString(); + const roomId = member.roomId.split(':')[0]; + const notification = `${member.name} joined the chat at ${chatTime} (room ID: ${roomId})`; + this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); // send notification to chat room + + this.sendTextMessage(member.roomId, `${member.name} has joined the chat.`); // revoke the other invitations + + this.uninviteFacilitators(member.roomId); // set transcript file + + if (this.config.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.localStorage.setItem(`${member.roomId}-transcript`, filepath); + } } } @@ -379,12 +467,13 @@ class OcrccBot { const memberCount = room.getJoinedMemberCount(); if (memberCount === 1) { - // just the bot + // just the bot left _logger.default.log("info", `LEAVING EMPTY ROOM ==> ${member.roomId}`); - this.client.leave(member.roomId); + this.deleteTranscript(member.userId, member.roomId); this.localStorage.removeItem(`${member.roomId}-facilitator`); this.localStorage.removeItem(`${member.roomId}-transcript`); + this.client.leave(member.roomId); } } }); diff --git a/dist/bot.test.js b/dist/bot.test.js index 3141093..04fe354 100644 --- a/dist/bot.test.js +++ b/dist/bot.test.js @@ -18,6 +18,36 @@ var _bot = _interopRequireDefault(require("./bot")); require('dotenv').config(); +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; +const { + MATRIX_SERVER_URL, + BOT_USERNAME, + BOT_USERID, + BOT_PASSWORD, + BOT_DISPLAY_NAME, + FACILITATOR_ROOM_ID, + CHAT_OFFLINE_MESSAGE, + CAPTURE_TRANSCRIPTS +} = process.env; +const botConfig = { + ENCRYPTION_CONFIG, + KICK_REASON, + BOT_ERROR_MESSAGE, + MAX_RETRIES, + MATRIX_SERVER_URL, + BOT_USERNAME, + BOT_USERID, + BOT_PASSWORD, + BOT_DISPLAY_NAME, + FACILITATOR_ROOM_ID, + CHAT_OFFLINE_MESSAGE, + CAPTURE_TRANSCRIPTS +}; const mockAppendFileSync = jest.fn(); fs.appendFileSync = mockAppendFileSync; describe('OcrccBot', () => { @@ -75,19 +105,17 @@ describe('OcrccBot', () => { _matrixJsSdk.mockGetGroupUsers.mockClear(); }); test('constructor should inititialize class variables', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); expect(bot.joinedRooms).toEqual([]); - expect(bot.awaitingFacilitator).toEqual({}); - expect(bot.activeChatrooms).toEqual({}); }); test('#createLocalStorage should have correct storage location', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); const localStorage = bot.createLocalStorage(); const localStoragePath = path.resolve(path.join(os.homedir(), ".local-storage", `matrix-chatbot-${process.env.BOT_USERNAME}`)); expect(localStorage._location).toBe(localStoragePath); }); test('#sendMessage should send a text message', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -103,7 +131,7 @@ describe('OcrccBot', () => { }); }); test('#inviteUserToRoom should add member to room and retry on rate limit error', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -114,7 +142,7 @@ describe('OcrccBot', () => { }); }); test('#kickUserFromRoom should remove member from room and retry on rate limit error', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -125,7 +153,7 @@ describe('OcrccBot', () => { }); }); test('#inviteFacilitators should invite all members from Facilitator room', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -142,7 +170,7 @@ describe('OcrccBot', () => { }); }); test('#uninviteFacilitators should remove all members that have not accepted the invite', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -159,7 +187,7 @@ describe('OcrccBot', () => { }); }); test('#handleBotCrash should notify rooms', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -173,11 +201,9 @@ describe('OcrccBot', () => { }); }); test('#writeToTranscript should parse event and write to transcript file', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); - bot.activeChatrooms['test_room_id'] = { - transcriptFile: '__mocks__/test_transcript.txt' - }; + bot.localStorage.setItem(`test_room_id-transcript`, '__mocks__/test_transcript.txt'); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); }); @@ -199,7 +225,7 @@ describe('OcrccBot', () => { }); }); test('#deleteOldDevices should delete old sessions', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockStartClient).toHaveBeenCalled(); @@ -217,7 +243,7 @@ describe('OcrccBot', () => { }); // TODO test listeners for membership events and message events test('#start should start bot and set up listeners', () => { - const bot = new _bot.default(); + const bot = new _bot.default(botConfig); bot.start(); (0, _waitForExpect.default)(() => { expect(_matrixJsSdk.mockLogin).toHaveBeenCalled(); diff --git a/dist/index.js b/dist/index.js index b687f41..caaf407 100644 --- a/dist/index.js +++ b/dist/index.js @@ -18,9 +18,7 @@ const { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, - CHAT_OFFLINE_MESSAGE, CAPTURE_TRANSCRIPTS } = process.env; const botConfig = { @@ -33,9 +31,7 @@ const botConfig = { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, - CHAT_OFFLINE_MESSAGE, CAPTURE_TRANSCRIPTS }; const bot = new _bot.default(botConfig); diff --git a/src/bot.js b/src/bot.js index 529853f..5e28fa7 100644 --- a/src/bot.js +++ b/src/bot.js @@ -64,9 +64,9 @@ class OcrccBot { } async sendMessage(roomId, content) { - logger.log("info", `SENDING MESSAGE: ${content.body}`) try { await this.client.sendMessage(roomId, content) + logger.log("info", `SENT MESSAGE: ${content.body}`) } catch(err) { switch (err["name"]) { case "UnknownDeviceError": @@ -293,7 +293,7 @@ class OcrccBot { const file = fs.readFileSync(transcriptFile); const stats = fs.statSync(transcriptFile); - const url = await this.client.uploadContent(file, { rawResponse: false, type: 'text/plain' }) + const url = await this.client.uploadContent(file, { rawResponse: false, name: filename }) logger.log('info', url) const content = { @@ -304,7 +304,8 @@ class OcrccBot { mimetype: 'text/plain' }, url: url.content_uri, - showToUser: senderId + showToUser: senderId, + mimetype: 'text/plain' }; this.sendMessage(roomId, content); @@ -388,15 +389,19 @@ class OcrccBot { !this.joinedRooms.includes(member.roomId) ) { try { - const room = await this.client.joinRoom(member.roomId) - logger.log("info", "AUTO JOINED ROOM => " + room.roomId) - const currentDate = new Date() - const chatDate = currentDate.toLocaleDateString() - const chatTime = currentDate.toLocaleTimeString() - const roomId = room.roomId.split(':')[0] - const notification = `Incoming support chat at ${chatTime} (room ID: ${roomId})` - this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); - this.inviteFacilitators(room.roomId) + const roomData = await this.client.getJoinedRooms() + const joinedRooms = roomData["joined_rooms"] + if (!joinedRooms.includes(member.roomId)) { + const room = await this.client.joinRoom(member.roomId) + logger.log("info", "AUTO JOINED ROOM => " + room.roomId) + const currentDate = new Date() + const chatDate = currentDate.toLocaleDateString() + const chatTime = currentDate.toLocaleTimeString() + const roomId = room.roomId.split(':')[0] + const notification = `Incoming support chat at ${chatTime} (room ID: ${roomId})` + this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); + this.inviteFacilitators(room.roomId) + } } catch(err) { logger.log("error", "ERROR JOINING ROOM => " + err) } diff --git a/src/bot.test.js b/src/bot.test.js index 17838c4..c312908 100644 --- a/src/bot.test.js +++ b/src/bot.test.js @@ -48,7 +48,6 @@ const { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, CHAT_OFFLINE_MESSAGE, CAPTURE_TRANSCRIPTS @@ -64,7 +63,6 @@ const botConfig = { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, CHAT_OFFLINE_MESSAGE, CAPTURE_TRANSCRIPTS diff --git a/src/index.js b/src/index.js index 5e808d3..1a11115 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,6 @@ const { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, CAPTURE_TRANSCRIPTS, } = process.env; @@ -26,7 +25,6 @@ const botConfig = { BOT_USERID, BOT_PASSWORD, BOT_DISPLAY_NAME, - FACILITATOR_GROUP_ID, FACILITATOR_ROOM_ID, CAPTURE_TRANSCRIPTS, }