use dotenv instead of config, sync with changes from glitch

This commit is contained in:
Sharon Kennedy 2020-03-02 17:52:25 -05:00
parent febd3feb61
commit 5876183d93
6 changed files with 172 additions and 183 deletions

7
.env.sample Normal file
View File

@ -0,0 +1,7 @@
MATRIX_SERVER_URL=
BOT_DISPLAY_NAME=
BOT_USERNAME=
BOT_PASSWORD=
BOT_USERID=
FACILITATOR_ROOM_ID=
CHAT_OFFLINE_MESSAGE=

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
config .env
node_modules node_modules
*.log *.log

View File

@ -4,12 +4,12 @@
"description": "Chatbot to manage interactions on OCRCC client chatbots", "description": "Chatbot to manage interactions on OCRCC client chatbots",
"main": "dist/ocrcc-chatbot.js", "main": "dist/ocrcc-chatbot.js",
"scripts": { "scripts": {
"start": "nodemon --exec babel-node src/index.js" "start": "nodemon -r dotenv/config --exec babel-node src/index.js"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"config": "^3.2.5", "dotenv": "^8.2.0",
"matrix-bot-sdk": "^0.5.0", "matrix-bot-sdk": "^0.5.0",
"matrix-js-sdk": "^5.0.0", "matrix-js-sdk": "^5.0.0",
"node-localstorage": "^2.1.5", "node-localstorage": "^2.1.5",

View File

@ -1,205 +1,194 @@
import * as fs from 'fs' import * as fs from "fs";
import * as os from 'os' import * as os from "os";
import * as path from 'path' import * as path from "path";
import * as util from 'util' import * as util from "util";
import { LocalStorage } from "node-localstorage"; 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 * 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 { class OcrccBot {
constructor() { constructor() {
this.awaitingAgreement = {} this.awaitingAgreement = {};
this.awaitingFacilitator = {} this.awaitingFacilitator = {};
this.client = matrix.createClient(config.get('homeserverUrl')) this.client = matrix.createClient(process.env.MATRIX_SERVER_URL);
this.joinedRooms = [];
} }
createLocalStorage() { createLocalStorage() {
const storageLoc = `matrix-chatbot-${config.get('username')}` const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`;
const dir = path.resolve(path.join(os.homedir(), ".local-storage")) const dir = path.resolve(path.join(os.homedir(), ".local-storage"));
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir); fs.mkdirSync(dir);
} }
const localStoragePath = path.resolve(path.join(dir, storageLoc)) const localStoragePath = path.resolve(path.join(dir, storageLoc));
return new LocalStorage(localStoragePath); return new LocalStorage(localStoragePath);
} }
sendMessage(roomId, msgText) { sendMessage(roomId, msgText) {
return this.client.sendTextMessage(roomId, msgText) return this.client
.then((res) => { .sendTextMessage(roomId, msgText)
logger.log('info', "Message sent") .then(res => {
logger.log('info', res) logger.log("info", "Message sent");
logger.log("info", res);
}) })
.catch((err) => { .catch(err => {
switch (err["name"]) { switch (err["name"]) {
case "UnknownDeviceError": case "UnknownDeviceError":
Object.keys(err.devices).forEach((userId) => { Object.keys(err.devices).forEach(userId => {
Object.keys(err.devices[userId]).map((deviceId) => { Object.keys(err.devices[userId]).map(deviceId => {
this.client.setDeviceVerified(userId, deviceId, true); this.client.setDeviceVerified(userId, deviceId, true);
}); });
}); });
return this.sendMessage(roomId, msgText) return this.sendMessage(roomId, msgText);
break; break;
default: default:
logger.log('error', "Error sending message"); logger.log("error", "Error sending message");
logger.log('error', err); logger.log("error", err);
break; 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) { inviteFacilitators(roomId) {
this.awaitingFacilitator[roomId] = true this.awaitingFacilitator[roomId] = true;
this.client.getJoinedRoomMembers(config.get('waitingRoomId')) let chatOffline = true;
.then((members) => { this.client
Object.keys(members["joined"]).forEach((member) => { .getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
if (member !== config.get('userId')) .then(members => {
this.client.invite(roomId, member) 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);
}
});
}) })
}) .then(() => {
// const notif = `There is a support seeker waiting. Go to https://riot.im/app/#/room/${roomId} to respond.` if (chatOffline) {
// sendMessage(waitingRoomId, notif) this.sendMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE);
}
})
.catch(err => {
logger.log("error", "ERROR GETTING ROOM MEMBERS");
logger.log("error", err);
});
} }
uninviteFacilitators(roomId) { uninviteFacilitators(roomId) {
this.awaitingFacilitator[roomId] = false this.awaitingFacilitator[roomId] = false;
this.client.getJoinedRoomMembers(config.get('waitingRoomId')) this.client
.then((allFacilitators) => { .getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
this.client.getJoinedRoomMembers(roomId) .then(allFacilitators => {
.then((roomMembers) => { this.client.getJoinedRoomMembers(roomId).then(roomMembers => {
const membersIds = Object.keys(roomMembers["joined"]) const membersIds = Object.keys(roomMembers["joined"]);
const facilitatorsIds = Object.keys(allFacilitators["joined"]) const facilitatorsIds = Object.keys(allFacilitators["joined"]);
facilitatorsIds.forEach((f) => { facilitatorsIds.forEach(f => {
if (!membersIds.includes(f)) { if (!membersIds.includes(f)) {
logger.log("info", "kicking out " + f + " from " + roomId) logger.log("info", "kicking out " + f + " from " + roomId);
this.client.kick(roomId, f, "A facilitator has already joined this chat.") this.client
.then(() => { .kick(roomId, f, "A facilitator has already joined this chat.")
logger.log("info", "Kick success") .then(() => {
}) logger.log("info", "Kick success");
.catch((err) => { })
logger.log("error", err) .catch(err => {
}) logger.log("error", err);
} });
}) }
});
});
}) })
}) .catch(err => logger.log("error", err));
} }
start() { start() {
const localStorage = this.createLocalStorage() const localStorage = this.createLocalStorage();
let deviceId = localStorage.getItem('deviceId')
this.client.login('m.login.password', { this.client
user: config.get('username'), .login("m.login.password", {
password: config.get('password'), user: process.env.BOT_USERNAME,
initial_device_display_name: config.get('botName'), password: process.env.BOT_PASSWORD,
deviceId: deviceId, initial_device_display_name: process.env.BOT_DISPLAY_NAME
}) })
.then((data) => { .then(data => {
const accessToken = data.access_token const accessToken = data.access_token;
const deviceId = data.device_id 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 = { this.client = matrix.createClient(opts);
baseUrl: config.get('homeserverUrl'), })
accessToken: accessToken, .catch(err => {
userId: config.get('userId'), logger.log("error", `Login error: ${err}`);
deviceId: deviceId, })
sessionStore: new matrix.WebStorageSessionStore(localStorage), .then(() =>
} this.client.initCrypto().catch(err => {
logger.log("error", `ERROR STARTING CRYPTO: ${err}`);
this.client = matrix.createClient(opts) })
}) )
.catch(err => { .then(() =>
logger.log('error', `Login error: ${err}`) this.client.getJoinedRooms().then(data => {
}) this.joinedRooms = data["joined_rooms"];
.then(() => this.client.initCrypto()) })
.then(() => { )
.then(() => {
// Automatically accept all room invitations // 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) => {
this.client.on("RoomMember.membership", (event, member) => { if (
if (member.membership === "invite" && member.userId === config.get('userId')) { member.membership === "invite" &&
logger.log("info", "Auto-joining room " + member.roomId) member.userId === process.env.BOT_USERID &&
this.client.joinRoom(member.roomId) !this.joinedRooms.includes(member.roomId)
.then(() => this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG)) ) {
.then(() => { logger.log("info", "Auto-joining room " + member.roomId);
if (member.roomId !== config.get('waitingRoomId')) { this.client
this.sendMessage(member.roomId, config.get('introMessage')) .joinRoom(member.roomId)
.then(() => this.sendHtmlMessage(member.roomId, `Please read the terms and conditions at ${config.get('termsUrl')}`, `Please read the full <a href="${config.get('termsUrl')}">terms and conditions</a>.`)) .then(room => {
.then(() => this.sendMessage(member.roomId, config.get('agreementMessage'))) this.sendMessage(
.then(() => this.awaitingAgreement[member.roomId] = true) process.env.FACILITATOR_ROOM_ID,
} `A support seeker requested a chat (Room ID: ${member.roomId})`
}) );
} this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG);
})
// When the first facilitator joins a support session, uninvite the other facilitators .then(() => this.inviteFacilitators(member.roomId));
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
}
} }
}
});
})
.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(); const bot = new OcrccBot();
bot.start() bot.start();

View File

@ -1,16 +1,16 @@
import winston from 'winston' import winston from "winston";
const logger = winston.createLogger({ const logger = winston.createLogger({
level: 'info', level: "info",
format: winston.format.json(), format: winston.format.json(),
defaultMeta: { service: 'user-service' }, defaultMeta: { service: "user-service" },
transports: [ transports: [
// //
// - Write all logs with level `error` and below to `error.log` // - Write all logs with level `error` and below to `error.log`
// - Write all logs with level `info` and below to `combined.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: "error.log", level: "error" }),
new winston.transports.File({ filename: 'combined.log' }) 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: // If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` // `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
// //
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== "production") {
logger.add(new winston.transports.Console({ logger.add(
format: winston.format.simple() new winston.transports.Console({
})); format: winston.format.simple()
})
);
} }
export default logger; export default logger;

View File

@ -1167,13 +1167,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 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: configstore@^3.0.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
@ -1358,6 +1351,11 @@ dot-prop@^4.1.0:
dependencies: dependencies:
is-obj "^1.0.0" 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: duplexer3@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" 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" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 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: json5@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6"