diff --git a/.env.sample b/.env.sample deleted file mode 100644 index 2538394..0000000 --- a/.env.sample +++ /dev/null @@ -1,7 +0,0 @@ -MATRIX_SERVER_URL= -BOT_DISPLAY_NAME= -BOT_USERNAME= -BOT_PASSWORD= -BOT_USERID= -FACILITATOR_ROOM_ID= -CAPTURE_TRANSCRIPTS \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d10baa..f504d94 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules *.log __mocks__/test_transcript.txt transcripts/*.txt +config.json \ No newline at end of file diff --git a/README.md b/README.md index 55684bd..c54a618 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,10 @@ # Safe Support Chat Bot -A simple Matrix bot that handles inviting, uninviting, and notifying Riot users on the recieving end of the [Safe Support chatbox](https://github.com/nomadic-labs/safesupport-chatbox). +A simple Matrix bot that handles inviting, uninviting, and notifying Riot users on the recieving end of the [Safe Support chatbox](https://github.com/nomadic-labs/safesupport-chatbox). + +The bot configuration file is `config.json`. It can also pull in user-set configurations from the Safe Support Chat Admin app. To do so, run the command `yarn setup` before starting the bot. -The bot can be configured with an `.env` file with the following variables: -``` -MATRIX_SERVER_URL= -BOT_DISPLAY_NAME= -BOT_USERNAME= -BOT_PASSWORD= -BOT_USERID= -FACILITATOR_ROOM_ID= -CHAT_OFFLINE_MESSAGE= -CAPTURE_TRANSCRIPTS= -``` ## What does the bot do? * The bot receives an invitation to every chatroom created by the embedded chatbox, and automatically accepts * Upon joining a new room, the bot invites all of the members of the Facilitators community @@ -29,7 +20,7 @@ CAPTURE_TRANSCRIPTS= ### Bot commands |Command|Response| ---- | --- +--- | --- |`!bot hi`|Bot responds with a greeting| |`!bot transcript`|Bot sends the chat transcript as a .txt file| |`!bot transcript please`|Bot happily sends the transcript :)| @@ -48,9 +39,14 @@ cd safesupport-bot yarn ``` -Copy the sample `.env` file and add in your own variables +Copy the sample config file and add in the missing values. ``` -cp .env.sample .env +cp sample.config.json config.json +``` + +Pull in the user-defined settings (if there are any). +``` +yarn setup ``` Start the local server diff --git a/dist/bot.js b/dist/bot.js index 4b9bb46..9d48e67 100644 --- a/dist/bot.js +++ b/dist/bot.js @@ -30,15 +30,15 @@ const BOT_SIGNAL_END_CHAT = 'END_CHAT'; const BOT_SIGNAL_CHAT_OFFLINE = 'CHAT_OFFLINE'; class OcrccBot { - constructor(botConfig) { - this.config = botConfig; - this.client = matrix.createClient(this.config.MATRIX_SERVER_URL); + constructor(config) { + this.config = config; + this.client = matrix.createClient(this.config.matrixServerUrl); this.joinedRooms = []; this.inactivityTimers = {}; } createLocalStorage() { - const storageLoc = `matrix-chatbot-${this.config.BOT_USERNAME}`; + const storageLoc = `matrix-chatbot-${this.config.botUsername}`; const dir = path.resolve(path.join(os.homedir(), ".local-storage")); if (!fs.existsSync(dir)) { @@ -112,17 +112,9 @@ class OcrccBot { } } - inviteUserToRoom(roomId, member) { - try { - this.client.invite(roomId, member); - } catch (err) { - this.handleBotCrash(roomId, err); - } - } - kickUserFromRoom(roomId, member) { try { - this.client.kick(roomId, member, this.config.KICK_REASON); + this.client.kick(roomId, member, this.config.kickReason); } catch (err) { this.handleBotCrash(roomId, err); @@ -130,27 +122,50 @@ class OcrccBot { } } + async inviteFacilitatorIfOnline(roomId, memberId) { + const user = this.client.getUser(memberId); + + if (user && user.presence !== "offline" && memberId !== this.config.botUserId) { + try { + this.client.invite(roomId, memberId); + + _logger.default.log("info", `CHAT INVITATION SENT TO ${memberId} FOR ROOM ${roomId}`); + + return memberId; + } catch (err) { + this.handleBotCrash(roomId, err); + return null; + } + } else { + return null; + } + } + async inviteFacilitators(roomId) { try { this.localStorage.setItem(`${roomId}-waiting`, 'true'); let invitations = []; - const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const roomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId); const members = Object.keys(roomMembers["joined"]); - members.forEach(memberId => { - const user = this.client.getUser(memberId); - if (user && user.presence !== "offline" && memberId !== this.config.BOT_USERID) { - invitations.push(memberId); - this.inviteUserToRoom(roomId, memberId); - } - }); + for (const memberId of members) { + const invited = await this.inviteFacilitatorIfOnline(roomId, memberId); + invitations.push(invited); + } - if (invitations.length > 0) { + if (invitations.filter(i => i).length > 0) { this.localStorage.setItem(`${roomId}-invitations`, invitations); } else { - _logger.default.log('info', "NO FACILITATORS ONLINE"); + this.sendBotSignal(roomId, BOT_SIGNAL_CHAT_OFFLINE); // send notification to Support Chat Notifications room - this.sendBotSignal(roomId, BOT_SIGNAL_CHAT_OFFLINE); + const currentDate = new Date(); + const closedTime = currentDate.toLocaleTimeString(); + const roomRef = roomId.split(':')[0]; + const notification = `No facilitators were online, chat closed at ${closedTime} (room ID: ${roomRef})`; + + _logger.default.log('info', `NO FACILITATORS ONLINE, CHAT CLOSED AT ${closedTime} (room ID: ${roomRef})`); + + this.sendTextMessage(this.config.facilitatorRoomId, notification); } } catch (err) { this.handleBotCrash(roomId, err); @@ -162,7 +177,7 @@ class OcrccBot { async uninviteFacilitators(roomId) { try { this.localStorage.removeItem(`${roomId}-waiting`); - const facilitatorsRoomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const facilitatorsRoomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId); const supportRoomMembers = await this.client.getJoinedRoomMembers(roomId); const roomMembersIds = Object.keys(supportRoomMembers["joined"]); const facilitatorsIds = Object.keys(facilitatorsRoomMembers["joined"]); @@ -181,10 +196,11 @@ class OcrccBot { handleBotCrash(roomId, error) { if (roomId) { - this.sendTextMessage(roomId, this.config.BOT_ERROR_MESSAGE); + this.sendTextMessage(roomId, this.config.botErrorMessage); + this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT); } - this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, `${this.config.BOT_DISPLAY_NAME} ran into an error: ${error}. Please verify that the chat service is working.`); + this.sendTextMessage(this.config.facilitatorRoomId, `${this.config.botDisplayName} ran into an error: ${error}. Please verify that the chat service is working.`); } handleMessageEvent(event) { @@ -199,7 +215,7 @@ class OcrccBot { const facilitatorId = this.localStorage.getItem(`${roomId}-facilitator`); - if (Boolean(facilitatorId) && sender !== this.config.BOT_USERID) { + if (Boolean(facilitatorId) && sender !== this.config.botUserId) { this.setInactivityTimeout(roomId); } // bot commands @@ -209,7 +225,7 @@ class OcrccBot { } // write to transcript - if (this.config.CAPTURE_TRANSCRIPTS) { + if (this.config.captureTranscripts) { return this.writeToTranscript(event); } } @@ -406,12 +422,12 @@ class OcrccBot { const auth = { session: err.data.session, type: "m.login.password", - user: this.config.BOT_USERID, + user: this.config.botUserId, identifier: { type: "m.id.user", - user: this.config.BOT_USERID + user: this.config.botUserId }, - password: this.config.BOT_PASSWORD + password: this.config.botPassword }; await this.client.deleteMultipleDevices(oldDevices, auth); @@ -429,7 +445,7 @@ class OcrccBot { async setMembershipListeners() { // Automatically accept all room invitations this.client.on("RoomMember.membership", async (event, member) => { - if (member.membership === "invite" && member.userId === this.config.BOT_USERID && !this.joinedRooms.includes(member.roomId)) { + if (member.membership === "invite" && member.userId === this.config.botUserId && !this.joinedRooms.includes(member.roomId)) { try { const roomData = await this.client.getJoinedRooms(); const joinedRooms = roomData["joined_rooms"]; @@ -444,7 +460,7 @@ class OcrccBot { const chatTime = inviteDate.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.sendTextMessage(this.config.facilitatorRoomId, notification); this.inviteFacilitators(room.roomId); this.setTimeoutforFacilitator(room.roomId); } @@ -453,10 +469,10 @@ class OcrccBot { } } - if (member.membership === "join" && member.userId !== this.config.BOT_USERID && this.localStorage.getItem(`${member.roomId}-waiting`)) { + if (member.membership === "join" && member.userId !== this.config.botUserId && this.localStorage.getItem(`${member.roomId}-waiting`)) { try { // make sure it's a facilitator joining - const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); + const roomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId); const members = Object.keys(roomMembers["joined"]); const isFacilitator = members.includes(member.userId); @@ -470,7 +486,7 @@ class OcrccBot { getContent: () => { return { users: { - [this.config.BOT_USERID]: 100, + [this.config.botUserId]: 100, [member.userId]: 50 } }; @@ -482,13 +498,13 @@ class OcrccBot { 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(this.config.facilitatorRoomId, 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) { + if (this.config.captureTranscripts) { const currentDate = new Date(); const dateOpts = { year: "numeric", @@ -509,17 +525,32 @@ class OcrccBot { } } - if (member.membership === "leave" && member.userId !== this.config.BOT_USERID) { + if (member.membership === "leave" && member.userId !== this.config.botUserId) { + // notify room if the facilitator has left + try { + const facilitatorId = this.localStorage.getItem(`${member.roomId}-facilitator`); + + if (member.userId === facilitatorId) { + this.sendTextMessage(member.roomId, `${member.name} has left the chat.`); // 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} left the chat at ${chatTime} (room ID: ${roomId})`; + await this.sendTextMessage(this.config.facilitatorRoomId, notification); + } + } catch (err) { + _logger.default.log("error", `ERROR NOTIFYING THAT FACLITATOR HAS LEFT THE ROOM ==> ${err}`); + } + const room = this.client.getRoom(member.roomId); if (!room) return; const roomMembers = await room.getJoinedMembers(); // array - - const facilitatorRoomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID); // object - - const isBotInRoom = Boolean(roomMembers.find(member => member.userId === this.config.BOT_USERID)); // leave if there is nobody in the room + // leave if there is nobody in the room try { const memberCount = roomMembers.length; + const isBotInRoom = Boolean(roomMembers.find(member => member.userId === this.config.botUserId)); if (memberCount === 1 && isBotInRoom) { // just the bot left @@ -533,31 +564,16 @@ class OcrccBot { } } catch (err) { return _logger.default.log("error", `ERROR LEAVING EMPTY ROOM ==> ${err}`); - } // notify room if the facilitator has left - - - try { - const facilitatorId = this.localStorage.getItem(`${member.roomId}-facilitator`); - - if (isBotInRoom && member.userId === facilitatorId) { - this.sendTextMessage(member.roomId, `${member.name} has left the chat.`); // 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} left the chat at ${chatTime} (room ID: ${roomId})`; - await this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); - } - } catch (err) { - _logger.default.log("error", `ERROR NOTIFYING THAT FACLITATOR HAS LEFT THE ROOM ==> ${err}`); } // send signal to close the chat if there are no facilitators in the room try { + const facilitatorRoomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId); // object + const facilitators = facilitatorRoomMembers['joined']; let facilitatorInRoom = false; roomMembers.forEach(member => { - if (member.userId !== this.config.BOT_USERID && Boolean(facilitators[member.userId])) { + if (member.userId !== this.config.botUserId && Boolean(facilitators[member.userId])) { facilitatorInRoom = true; } }); @@ -579,12 +595,18 @@ class OcrccBot { const stillWaiting = this.localStorage.getItem(`${roomId}-waiting`); if (stillWaiting) { - _logger.default.log("info", `FACILITATOR DID NOT JOIN CHAT WITHIN TIME LIMIT, SENDING SIGNAL TO END CHAT`); + await this.sendTextMessage(roomId, this.config.chatNotAvailableMessage); + this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT); // send notification to Support Chat Notifications room - await this.sendTextMessage(roomId, this.config.CHAT_NOT_AVAILABLE_MESSAGE); - this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT); + const currentDate = new Date(); + const closedTime = currentDate.toLocaleTimeString(); + const roomRef = roomId.split(':')[0]; + const notification = `No facilitators joined the chat within the maximum wait time, chat closed at ${closedTime} (room ID: ${roomRef})`; + this.sendTextMessage(this.config.facilitatorRoomId, notification); + + _logger.default.log("info", `NO FACILITATORS JOINED THE CHAT WITHIN THE MAXIMUM WAIT TIME, CHAT CLOSED AT ${closedTime} (room ID: ${roomRef})`); } - }, this.config.MAX_WAIT_TIME); + }, this.config.maxWaitTime * 1000); // convert seconds to milliseconds } setInactivityTimeout(roomId) { @@ -597,9 +619,10 @@ class OcrccBot { const newTimeout = setTimeout(async () => { _logger.default.log("info", `CHAT IS INACTIVE, SENDING SIGNAL TO END CHAT`); - await this.sendTextMessage(roomId, `This chat has been closed due to inactivity.`); + await this.sendTextMessage(roomId, this.config.chatInactiveMessage); this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT); - }, this.config.MAX_INACTIVE); + }, this.config.maxInactiveTime * 1000); // convert seconds to milliseconds + this.inactivityTimers[roomId] = newTimeout; } @@ -640,18 +663,18 @@ class OcrccBot { const localStorage = this.createLocalStorage(); this.localStorage = localStorage; const auth = { - user: this.config.BOT_USERNAME, - password: this.config.BOT_PASSWORD, - initial_device_display_name: this.config.BOT_DISPLAY_NAME + user: this.config.botUsername, + password: this.config.botPassword, + initial_device_display_name: this.config.botDisplayName }; const account = await this.client.login("m.login.password", auth); _logger.default.log("info", `ACCOUNT ==> ${JSON.stringify(account)}`); let opts = { - baseUrl: this.config.MATRIX_SERVER_URL, + baseUrl: this.config.matrixServerUrl, accessToken: account.access_token, - userId: this.config.BOT_USERID, + userId: this.config.botUserId, deviceId: account.device_id, sessionStore: new matrix.WebStorageSessionStore(localStorage) }; diff --git a/dist/index.js b/dist/index.js index e785bd0..d2ef2e9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2,48 +2,14 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +var _config = _interopRequireDefault(require("../config.json")); + 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, - CAPTURE_TRANSCRIPTS, - CHAT_NOT_AVAILABLE_MESSAGE, - MAX_WAIT_TIME, - MAX_INACTIVE -} = 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, - CAPTURE_TRANSCRIPTS, - CHAT_NOT_AVAILABLE_MESSAGE, - MAX_WAIT_TIME, - MAX_INACTIVE -}; -const bot = new _bot.default(botConfig); +const bot = new _bot.default(_config.default); try { bot.start(); } catch (err) { - console.log("AAAAAAAAAAAAA", err); + console.log("Unable to start bot", err); } \ No newline at end of file diff --git a/dist/setup.js b/dist/setup.js new file mode 100644 index 0000000..c05f019 --- /dev/null +++ b/dist/setup.js @@ -0,0 +1,54 @@ +"use strict"; + +const fs = require('fs'); + +const fetch = require('node-fetch'); + +const config = require('../config.json'); + +const getSettings = async () => { + try { + const url = `${config.settingsEndpoint}?homeserver=${encodeURIComponent(config.matrixServerUrl)}`; + + if (!config.matrixServerUrl) { + throw new Error("The matrix server url is not provided"); + } + + console.log(`Fetching settings for ${config.matrixServerUrl}`); + const res = await fetch(url); + const data = await res.json(); + const { + fields, + schedule = [] + } = data; + return Object.entries(fields).reduce((settingsObj, [k, v]) => { + const [scope, key] = k.split('_'); + + if (scope === 'platform') { + settingsObj[key] = v; + } + + return settingsObj; + }, {}); + } catch (err) { + console.log("Error fetching settings", err); + return null; + } +}; + +const writeConfig = async () => { + const settings = await getSettings(); + + if (!settings) { + return console.log('No settings to update'); + } + + const updatedSettings = Object.assign(config, settings); + fs.writeFile('config.json', JSON.stringify(updatedSettings, null, 2), function (err) { + if (err) return console.log("Error updating settings", err); + console.log(`Updated settings to config.json`); + console.log(updatedSettings); + }); +}; + +writeConfig(); \ No newline at end of file diff --git a/package.json b/package.json index eae03c0..2186953 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "develop": "nodemon --exec babel-node src/index.js", "build": "babel src -d dist", + "setup": "node src/setup.js", "start": "yarn build && node dist/index.js", "test": "jest" }, @@ -14,6 +15,7 @@ "dependencies": { "dotenv": "^8.2.0", "matrix-js-sdk": "^6.2.1", + "node-fetch": "^2.6.1", "node-localstorage": "^2.1.5", "node-webcrypto-ossl": "^2.1.0", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", diff --git a/sample.config.json b/sample.config.json new file mode 100644 index 0000000..e127c34 --- /dev/null +++ b/sample.config.json @@ -0,0 +1,16 @@ +{ + "matrixServerUrl": "", + "settingsEndpoint": "", + "facilitatorRoomId": "", + "kickReason": "A facilitator has already joined this chat.", + "botErrorMessage": "Something went wrong on our end, please restart the chat and try again.", + "botUserId": "", + "botUsername": "", + "botPassword": "", + "botDisplayName": "Help Bot", + "captureTranscripts": true, + "chatNotAvailableMessage": "The support chat is not available right now.", + "chatInactiveMessage": "This chat has been closed due to inactivity.", + "maxWaitTime": 180, + "maxInactiveTime": 3600 +} \ No newline at end of file diff --git a/src/bot.js b/src/bot.js index 66432ab..f8851f7 100644 --- a/src/bot.js +++ b/src/bot.js @@ -14,17 +14,16 @@ import encrypt from "./encrypt-attachment"; const BOT_SIGNAL_END_CHAT = 'END_CHAT' const BOT_SIGNAL_CHAT_OFFLINE = 'CHAT_OFFLINE' - class OcrccBot { - constructor(botConfig) { - this.config = botConfig - this.client = matrix.createClient(this.config.MATRIX_SERVER_URL); + constructor(config) { + this.config = config + this.client = matrix.createClient(this.config.matrixServerUrl); this.joinedRooms = []; this.inactivityTimers = {}; } createLocalStorage() { - const storageLoc = `matrix-chatbot-${this.config.BOT_USERNAME}`; + const storageLoc = `matrix-chatbot-${this.config.botUsername}`; const dir = path.resolve(path.join(os.homedir(), ".local-storage")); if (!fs.existsSync(dir)) { fs.mkdirSync(dir); @@ -91,47 +90,59 @@ class OcrccBot { } } - inviteUserToRoom(roomId, member) { - try { - this.client.invite(roomId, member) - } catch(err) { - this.handleBotCrash(roomId, err); - } - } - kickUserFromRoom(roomId, member) { try { - this.client.kick(roomId, member, this.config.KICK_REASON) + this.client.kick(roomId, member, this.config.kickReason) } catch(err) { this.handleBotCrash(roomId, err); logger.log("error", `ERROR KICKING OUT MEMBER: ${err}`); } } + async inviteFacilitatorIfOnline(roomId, memberId) { + const user = this.client.getUser(memberId); + if ( + user && + (user.presence !== "offline") && + memberId !== this.config.botUserId + ) { + try { + this.client.invite(roomId, memberId) + logger.log("info", `CHAT INVITATION SENT TO ${memberId} FOR ROOM ${roomId}`) + return memberId + } catch(err) { + this.handleBotCrash(roomId, err); + return null + } + } else { + return null + } + } + async inviteFacilitators(roomId) { try { this.localStorage.setItem(`${roomId}-waiting`, 'true') let invitations = [] - const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID) + const roomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId) const members = Object.keys(roomMembers["joined"]); - members.forEach(memberId => { - const user = this.client.getUser(memberId); - if ( - user && - (user.presence !== "offline") && - memberId !== this.config.BOT_USERID - ) { - invitations.push(memberId) - this.inviteUserToRoom(roomId, memberId); - } - }); + for (const memberId of members) { + const invited = await this.inviteFacilitatorIfOnline(roomId, memberId) + invitations.push(invited) + } - if (invitations.length > 0) { + if (invitations.filter(i => i).length > 0) { this.localStorage.setItem(`${roomId}-invitations`, invitations) } else { - logger.log('info', "NO FACILITATORS ONLINE") this.sendBotSignal(roomId, BOT_SIGNAL_CHAT_OFFLINE) + + // send notification to Support Chat Notifications room + const currentDate = new Date() + const closedTime = currentDate.toLocaleTimeString() + const roomRef = roomId.split(':')[0] + const notification = `No facilitators were online, chat closed at ${closedTime} (room ID: ${roomRef})` + logger.log('info', `NO FACILITATORS ONLINE, CHAT CLOSED AT ${closedTime} (room ID: ${roomRef})`) + this.sendTextMessage(this.config.facilitatorRoomId, notification); } } catch(err) { @@ -144,7 +155,7 @@ class OcrccBot { async uninviteFacilitators(roomId) { try { this.localStorage.removeItem(`${roomId}-waiting`) - const facilitatorsRoomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID) + const facilitatorsRoomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId) const supportRoomMembers = await this.client.getJoinedRoomMembers(roomId) const roomMembersIds = Object.keys(supportRoomMembers["joined"]); @@ -166,12 +177,13 @@ class OcrccBot { handleBotCrash(roomId, error) { if (roomId) { - this.sendTextMessage(roomId, this.config.BOT_ERROR_MESSAGE); + this.sendTextMessage(roomId, this.config.botErrorMessage); + this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT) } this.sendTextMessage( - this.config.FACILITATOR_ROOM_ID, - `${this.config.BOT_DISPLAY_NAME} ran into an error: ${error}. Please verify that the chat service is working.` + this.config.facilitatorRoomId, + `${this.config.botDisplayName} ran into an error: ${error}. Please verify that the chat service is working.` ); } @@ -187,7 +199,7 @@ class OcrccBot { // if it's a chat message and the facilitator has joined, reset the inactivity timeout const facilitatorId = this.localStorage.getItem(`${roomId}-facilitator`) - if (Boolean(facilitatorId) && sender !== this.config.BOT_USERID) { + if (Boolean(facilitatorId) && sender !== this.config.botUserId) { this.setInactivityTimeout(roomId) } @@ -197,7 +209,7 @@ class OcrccBot { } // write to transcript - if (this.config.CAPTURE_TRANSCRIPTS) { + if (this.config.captureTranscripts) { return this.writeToTranscript(event); } } @@ -417,9 +429,9 @@ class OcrccBot { const auth = { session: err.data.session, type: "m.login.password", - user: this.config.BOT_USERID, - identifier: { type: "m.id.user", user: this.config.BOT_USERID }, - password: this.config.BOT_PASSWORD + user: this.config.botUserId, + identifier: { type: "m.id.user", user: this.config.botUserId }, + password: this.config.botPassword }; await this.client.deleteMultipleDevices(oldDevices, auth) @@ -438,7 +450,7 @@ class OcrccBot { this.client.on("RoomMember.membership", async (event, member) => { if ( member.membership === "invite" && - member.userId === this.config.BOT_USERID && + member.userId === this.config.botUserId && !this.joinedRooms.includes(member.roomId) ) { try { @@ -452,7 +464,7 @@ class OcrccBot { const chatTime = inviteDate.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.sendTextMessage(this.config.facilitatorRoomId, notification); this.inviteFacilitators(room.roomId) this.setTimeoutforFacilitator(room.roomId) } @@ -463,12 +475,12 @@ class OcrccBot { if ( member.membership === "join" && - member.userId !== this.config.BOT_USERID && + member.userId !== this.config.botUserId && this.localStorage.getItem(`${member.roomId}-waiting`) ) { try { // make sure it's a facilitator joining - const roomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID) + const roomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId) const members = Object.keys(roomMembers["joined"]); const isFacilitator = members.includes(member.userId) @@ -482,7 +494,7 @@ class OcrccBot { getContent: () => { return { users: { - [this.config.BOT_USERID]: 100, + [this.config.botUserId]: 100, [member.userId]: 50 } }; @@ -495,7 +507,7 @@ class OcrccBot { 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); + this.sendTextMessage(this.config.facilitatorRoomId, notification); // send notification to chat room this.sendTextMessage( @@ -507,7 +519,7 @@ class OcrccBot { this.uninviteFacilitators(member.roomId); // set transcript file - if (this.config.CAPTURE_TRANSCRIPTS) { + if (this.config.captureTranscripts) { const currentDate = new Date(); const dateOpts = { year: "numeric", @@ -530,18 +542,36 @@ class OcrccBot { if ( member.membership === "leave" && - member.userId !== this.config.BOT_USERID + member.userId !== this.config.botUserId ) { + + // notify room if the facilitator has left + try { + const facilitatorId = this.localStorage.getItem(`${member.roomId}-facilitator`) + if (member.userId === facilitatorId) { + this.sendTextMessage( + member.roomId, + `${member.name} has left the chat.` + ); + // 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} left the chat at ${chatTime} (room ID: ${roomId})` + await this.sendTextMessage(this.config.facilitatorRoomId, notification); + } + } catch(err) { + logger.log("error", `ERROR NOTIFYING THAT FACLITATOR HAS LEFT THE ROOM ==> ${err}`); + } + const room = this.client.getRoom(member.roomId) if (!room) return; - const roomMembers = await room.getJoinedMembers() // array - const facilitatorRoomMembers = await this.client.getJoinedRoomMembers(this.config.FACILITATOR_ROOM_ID) // object - const isBotInRoom = Boolean(roomMembers.find(member => member.userId === this.config.BOT_USERID)) // leave if there is nobody in the room try { const memberCount = roomMembers.length + const isBotInRoom = Boolean(roomMembers.find(member => member.userId === this.config.botUserId)) if (memberCount === 1 && isBotInRoom) { // just the bot left logger.log("info", `LEAVING EMPTY ROOM: ${member.roomId}`); this.deleteTranscript(member.userId, member.roomId); @@ -554,32 +584,14 @@ class OcrccBot { return logger.log("error", `ERROR LEAVING EMPTY ROOM ==> ${err}`); } - // notify room if the facilitator has left - try { - const facilitatorId = this.localStorage.getItem(`${member.roomId}-facilitator`) - if (isBotInRoom && member.userId === facilitatorId) { - this.sendTextMessage( - member.roomId, - `${member.name} has left the chat.` - ); - // 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} left the chat at ${chatTime} (room ID: ${roomId})` - await this.sendTextMessage(this.config.FACILITATOR_ROOM_ID, notification); - } - } catch(err) { - logger.log("error", `ERROR NOTIFYING THAT FACLITATOR HAS LEFT THE ROOM ==> ${err}`); - } - // send signal to close the chat if there are no facilitators in the room try { + const facilitatorRoomMembers = await this.client.getJoinedRoomMembers(this.config.facilitatorRoomId) // object const facilitators = facilitatorRoomMembers['joined'] let facilitatorInRoom = false; roomMembers.forEach(member => { - if (member.userId !== this.config.BOT_USERID && Boolean(facilitators[member.userId])) { + if (member.userId !== this.config.botUserId && Boolean(facilitators[member.userId])) { facilitatorInRoom = true } }) @@ -600,14 +612,21 @@ class OcrccBot { setTimeout(async() => { const stillWaiting = this.localStorage.getItem(`${roomId}-waiting`) if (stillWaiting) { - logger.log("info", `FACILITATOR DID NOT JOIN CHAT WITHIN TIME LIMIT, SENDING SIGNAL TO END CHAT`); await this.sendTextMessage( roomId, - this.config.CHAT_NOT_AVAILABLE_MESSAGE + this.config.chatNotAvailableMessage ); this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT) + + // send notification to Support Chat Notifications room + const currentDate = new Date() + const closedTime = currentDate.toLocaleTimeString() + const roomRef = roomId.split(':')[0] + const notification = `No facilitators joined the chat within the maximum wait time, chat closed at ${closedTime} (room ID: ${roomRef})`; + this.sendTextMessage(this.config.facilitatorRoomId, notification); + logger.log("info", `NO FACILITATORS JOINED THE CHAT WITHIN THE MAXIMUM WAIT TIME, CHAT CLOSED AT ${closedTime} (room ID: ${roomRef})`); } - }, this.config.MAX_WAIT_TIME) + }, this.config.maxWaitTime * 1000) // convert seconds to milliseconds } setInactivityTimeout(roomId) { @@ -621,10 +640,10 @@ class OcrccBot { logger.log("info", `CHAT IS INACTIVE, SENDING SIGNAL TO END CHAT`); await this.sendTextMessage( roomId, - `This chat has been closed due to inactivity.` + this.config.chatInactiveMessage ); this.sendBotSignal(roomId, BOT_SIGNAL_END_CHAT) - }, this.config.MAX_INACTIVE) + }, this.config.maxInactiveTime * 1000) // convert seconds to milliseconds this.inactivityTimers[roomId] = newTimeout; } @@ -665,17 +684,17 @@ class OcrccBot { this.localStorage = localStorage const auth = { - user: this.config.BOT_USERNAME, - password: this.config.BOT_PASSWORD, - initial_device_display_name: this.config.BOT_DISPLAY_NAME + user: this.config.botUsername, + password: this.config.botPassword, + initial_device_display_name: this.config.botDisplayName } const account = await this.client.login("m.login.password", auth) logger.log("info", `ACCOUNT ==> ${JSON.stringify(account)}`); let opts = { - baseUrl: this.config.MATRIX_SERVER_URL, + baseUrl: this.config.matrixServerUrl, accessToken: account.access_token, - userId: this.config.BOT_USERID, + userId: this.config.botUserId, deviceId: account.device_id, sessionStore: new matrix.WebStorageSessionStore(localStorage) }; diff --git a/src/index.js b/src/index.js index 68fbd4c..e23dbf5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,46 +1,9 @@ -require('dotenv').config() +import config from '../config.json'; +import OcrccBot from './bot'; -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, - CAPTURE_TRANSCRIPTS, - CHAT_NOT_AVAILABLE_MESSAGE, - MAX_WAIT_TIME, - MAX_INACTIVE, -} = 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, - CAPTURE_TRANSCRIPTS, - CHAT_NOT_AVAILABLE_MESSAGE, - MAX_WAIT_TIME, - MAX_INACTIVE, -} - -import OcrccBot from './bot' - -const bot = new OcrccBot(botConfig); +const bot = new OcrccBot(config); try { bot.start(); } catch(err) { - console.log("AAAAAAAAAAAAA", err) + console.log("Unable to start bot", err) } diff --git a/src/setup.js b/src/setup.js new file mode 100644 index 0000000..e65d490 --- /dev/null +++ b/src/setup.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const fetch = require('node-fetch'); +const config = require('../config.json'); + +const getSettings = async () => { + try { + const url = `${config.settingsEndpoint}?homeserver=${encodeURIComponent(config.matrixServerUrl)}`; + + if (!config.matrixServerUrl) { + throw new Error("The matrix server url is not provided") + } + + console.log(`Fetching settings for ${config.matrixServerUrl}`); + + const res = await fetch(url); + const data = await res.json(); + + const { fields, schedule = [] } = data; + + return Object.entries(fields).reduce(((settingsObj, [k,v]) => { + const [scope, key] = k.split('_'); + + if (scope === 'platform') { + settingsObj[key] = v; + } + + return settingsObj + }), {}); + } catch (err) { + console.log("Error fetching settings", err); + return null + } +}; + +const writeConfig = async () => { + const settings = await getSettings() + if (!settings) { + return console.log('No settings to update') + } + + const updatedSettings = Object.assign(config, settings) + + fs.writeFile('config.json', JSON.stringify(updatedSettings, null, 2), function (err) { + if (err) return console.log("Error updating settings", err); + console.log(`Updated settings to config.json`); + console.log(updatedSettings); + }); +} + +writeConfig(); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 492f98c..4cc9045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3954,6 +3954,11 @@ node-environment-flags@^1.0.5: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" +node-fetch@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"