rewuite to use async

This commit is contained in:
Sharon Kennedy 2020-04-22 01:35:34 -04:00
parent fefda571d6
commit 242d32639a
6 changed files with 786 additions and 660 deletions

View File

@ -1,8 +0,0 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"dynamic-import-node"
]
}

View File

@ -21,9 +21,20 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4", "@babel/core": "7.7.7",
"@babel/node": "^7.8.4", "@babel/node": "^7.8.7",
"@babel/preset-env": "^7.8.4", "@babel/plugin-proposal-class-properties": "7.7.4",
"@babel/plugin-proposal-decorators": "7.7.4",
"@babel/plugin-proposal-export-namespace-from": "7.7.4",
"@babel/plugin-proposal-function-sent": "7.7.4",
"@babel/plugin-proposal-json-strings": "7.7.4",
"@babel/plugin-proposal-numeric-separator": "7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
"@babel/plugin-proposal-throw-expressions": "7.7.4",
"@babel/plugin-syntax-dynamic-import": "7.7.4",
"@babel/plugin-syntax-import-meta": "7.7.4",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
"babel-plugin-dynamic-import-node": "^2.3.0", "babel-plugin-dynamic-import-node": "^2.3.0",
"jest": "^25.1.0", "jest": "^25.1.0",
@ -32,6 +43,38 @@
"wait-for-expect": "^3.0.2" "wait-for-expect": "^3.0.2"
}, },
"jest": { "jest": {
"testPathIgnorePatterns": ["dist"] "testPathIgnorePatterns": [
"dist"
]
},
"babel": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "12"
}
}
]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-json-strings",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-runtime"
]
} }
} }

View File

@ -10,22 +10,18 @@ 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 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 { class OcrccBot {
constructor() { constructor(botConfig) {
this.config = botConfig
this.awaitingFacilitator = {}; this.awaitingFacilitator = {};
this.client = matrix.createClient(process.env.MATRIX_SERVER_URL); this.client = matrix.createClient(this.config.MATRIX_SERVER_URL);
this.joinedRooms = []; this.joinedRooms = [];
this.activeChatrooms = {}; this.activeChatrooms = {};
} }
createLocalStorage() { createLocalStorage() {
const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`; const storageLoc = `matrix-chatbot-${this.config.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);
@ -44,150 +40,102 @@ class OcrccBot {
this.sendMessage(roomId, content); this.sendMessage(roomId, content);
} }
sendMessage(roomId, content) { async sendMessage(roomId, content) {
logger.log("info", `SENDING MESSAGE: ${content.body}`)
return this.client.sendMessage(roomId, content).catch(err => { return this.client.sendMessage(roomId, content).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(async deviceId => {
this.client.setDeviceVerified(userId, deviceId, true); await this.client.setDeviceVerified(userId, deviceId, true);
}); });
}); });
return this.sendMessage(roomId, content); return this.sendMessage(roomId, content);
break; break;
default: default:
logger.log("error", `ERROR SENDING MESSAGE: ${err}`); logger.log("error", `ERROR SENDING MESSAGE: ${err}`);
this.handleBotCrash(roomId, err);
break; break;
} }
}); });
} }
inviteUserToRoom(client, roomId, member, retries = 0) { inviteUserToRoom(roomId, member) {
logger.log("info", "INVITING MEMBER: " + member); try {
if (retries > MAX_RETRIES) { this.client.invite(roomId, member)
this.handleBotCrash(roomId, "Rate limit exceeded for bot account"); } catch(err) {
return logger.log( this.handleBotCrash(roomId, err);
"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) { kickUserFromRoom(roomId, member) {
logger.log("info", "KICKING OUT MEMBER: " + member); try {
if (retries > MAX_RETRIES) { this.client.kick(roomId, member, this.config.KICK_REASON)
this.handleBotCrash(roomId, "Rate limit exceeded for bot account."); } catch(err) {
return logger.log( this.handleBotCrash(roomId, err);
"error", logger.log("error", `ERROR KICKING OUT MEMBER: ${err}`);
`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) { async inviteFacilitators(roomId) {
this.awaitingFacilitator[roomId] = true; this.awaitingFacilitator[roomId] = true;
let chatOffline = true; let chatOffline = true;
this.client
.getGroupUsers(process.env.FACILITATOR_GROUP_ID) try {
.then(res => { const data = await this.client.getGroupUsers(this.config.FACILITATOR_GROUP_ID)
const members = res.chunk; const members = data.chunk
let onlineMembersCount = 0;
members.forEach(member => { members.forEach(member => {
const memberId = member.user_id; const memberId = member.user_id;
const user = this.client.getUser(memberId); const user = this.client.getUser(memberId);
if ( if (
user && user &&
user.presence === "online" && user.presence === "online" &&
memberId !== process.env.BOT_USERID memberId !== this.config.BOT_USERID
) { ) {
chatOffline = false; chatOffline = false;
this.inviteUserToRoom(this.client, roomId, memberId); this.inviteUserToRoom(roomId, memberId);
}
});
})
.then(() => {
if (chatOffline) {
this.sendTextMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE);
} }
})
.catch(err => {
this.handleBotCrash(roomId, err);
logger.log("error", `ERROR GETTING FACILITATORS: ${err}`);
}); });
if (chatOffline) {
logger.log('info', "CHAT OFFLINE!")
this.sendTextMessage(roomId, this.config.CHAT_OFFLINE_MESSAGE);
}
} catch(err) {
this.handleBotCrash(roomId, err);
logger.log("error", `ERROR GETTING FACILITATORS: ${err}`);
}
} }
uninviteFacilitators(roomId) { async uninviteFacilitators(roomId) {
this.awaitingFacilitator[roomId] = false; this.awaitingFacilitator[roomId] = false;
this.client
.getGroupUsers(process.env.FACILITATOR_GROUP_ID) try {
.then(groupUsers => { const groupUsers = await this.client.getGroupUsers(this.config.FACILITATOR_GROUP_ID)
this.client.getJoinedRoomMembers(roomId).then(roomMembers => { const roomMembers = await this.client.getJoinedRoomMembers(roomId)
const membersIds = Object.keys(roomMembers["joined"]);
const facilitators = groupUsers.chunk; const membersIds = Object.keys(roomMembers["joined"]);
const facilitatorsIds = facilitators.map(f => f.user_id); const facilitatorsIds = groupUsers.chunk.map(f => f.user_id);
facilitatorsIds.forEach(f => {
if (!membersIds.includes(f)) { facilitatorsIds.forEach(f => {
this.kickUserFromRoom(this.client, roomId, f); if (!membersIds.includes(f)) {
} this.kickUserFromRoom(roomId, f);
}); }
});
})
.catch(err => {
this.handleBotCrash(roomId, err);
logger.log("error", err);
}); });
} catch(err) {
this.handleBotCrash(roomId, err);
logger.log("ERROR UNINVITING FACILITATORS", err);
}
} }
handleBotCrash(roomId, error) { handleBotCrash(roomId, error) {
if (roomId) { if (roomId) {
this.sendTextMessage(roomId, BOT_ERROR_MESSAGE); this.sendTextMessage(roomId, this.config.BOT_ERROR_MESSAGE);
} }
this.sendTextMessage( this.sendTextMessage(
process.env.FACILITATOR_ROOM_ID, this.config.FACILITATOR_ROOM_ID,
`The Help Bot ran into an error: ${error}. Please verify that the chat service is working.` `The Help Bot ran into an error: ${error}. Please verify that the chat service is working.`
); );
} }
@ -206,7 +154,7 @@ class OcrccBot {
} }
// write to transcript // write to transcript
if (process.env.CAPTURE_TRANSCRIPTS) { if (this.config.CAPTURE_TRANSCRIPTS) {
return this.writeToTranscript(event); return this.writeToTranscript(event);
} }
} }
@ -268,109 +216,112 @@ class OcrccBot {
} }
} }
sendTranscript(senderId, roomId) { async sendTranscript(senderId, roomId) {
const transcriptFile = this.activeChatrooms[roomId] try {
? this.activeChatrooms[roomId].transcriptFile const transcriptFile = this.activeChatrooms[roomId]
: false; ? this.activeChatrooms[roomId].transcriptFile
if (!transcriptFile) { : false;
this.sendTextMessage( if (!transcriptFile) {
roomId, this.sendTextMessage(
"There is no transcript for this chat.", roomId,
senderId "There is no transcript for this chat.",
); senderId
} );
}
const filename = path.basename(transcriptFile) || "Transcript"; const filename = path.basename(transcriptFile) || "Transcript";
const stream = fs.createReadStream(transcriptFile); const stream = fs.createReadStream(transcriptFile);
this.client const contentUrl = await this.client.uploadContent({
.uploadContent({
stream: stream, stream: stream,
name: filename name: filename
}) })
.then(contentUrl => {
const content = {
msgtype: "m.file",
body: filename,
url: JSON.parse(contentUrl).content_uri,
showToUser: senderId
};
this.sendMessage(roomId, content); const content = {
}) msgtype: "m.file",
.catch(err => { body: filename,
logger.log("error", `ERROR UPLOADING CONTENT: ${err}`); url: JSON.parse(contentUrl).content_uri,
this.sendTextMessage( showToUser: senderId
roomId, };
"There was an error uploading the transcript.",
senderId this.sendMessage(roomId, content);
); } catch(err) {
}); logger.log("error", `ERROR UPLOADING CONTENT: ${err}`);
this.sendTextMessage(
roomId,
"There was an error uploading the transcript.",
senderId
);
}
} }
deleteOldDevices() { async deleteOldDevices() {
return this.client.getDevices().then(data => { const currentDeviceId = this.client.getDeviceId();
const currentDeviceId = this.client.getDeviceId(); const deviceData = await this.client.getDevices()
const allDeviceIds = data.devices.map(d => d.device_id); const allDeviceIds = deviceData.devices.map(d => d.device_id)
const oldDevices = allDeviceIds.filter(id => id !== currentDeviceId); const oldDevices = allDeviceIds.filter(id => id !== currentDeviceId);
logger.log("info", `DELETING OLD DEVICES: ${oldDevices}`);
this.client.deleteMultipleDevices(oldDevices).catch(err => { try {
const auth = { await this.client.deleteMultipleDevices(oldDevices)
session: err.data.session, } catch(err) {
type: "m.login.password", logger.log("info", "RETRYING DELETE OLD DEVICES WITH AUTH")
user: process.env.BOT_USERID, const auth = {
identifier: { type: "m.id.user", user: process.env.BOT_USERID }, session: err.data.session,
password: process.env.BOT_PASSWORD type: "m.login.password",
}; user: this.config.BOT_USERID,
this.client identifier: { type: "m.id.user", user: this.config.BOT_USERID },
.deleteMultipleDevices(oldDevices, auth) password: this.config.BOT_PASSWORD
.then(() => logger.log("info", "DELETED OLD DEVICES")) };
.catch(err => {
if (err.errcode === "M_LIMIT_EXCEEDED") { await this.client.deleteMultipleDevices(oldDevices, auth)
const delay = err.retry_after_ms || 2000; logger.log("info", "DELETED OLD DEVICES")
logger.log("info", `RETRYING DELETE OLD DEVICES: ${oldDevices}`); }
setTimeout(() => {
this.client.deleteMultipleDevices(oldDevices);
}, delay);
} else {
logger.log(
"error",
`ERROR DELETING OLD DEVICES: ${JSON.stringify(err.data)}`
);
}
});
});
});
} }
setMembershipListeners() { async leaveOldRooms() {
const roomData = await this.client.getJoinedRooms()
const joinedRoomsIds = roomData["joined_rooms"]
this.joinedRooms = joinedRoomsIds
logger.log("info", `LEAVING ROOMS ${joinedRoomsIds}`)
joinedRoomsIds.forEach(async(roomId) => {
if (roomId === this.config.FACILITATOR_ROOM_ID) return;
try {
await this.client.leave(roomId)
} catch(err) {
logger.log("error", `ERROR LEAVING ROOM => ${err}`)
}
})
}
async setMembershipListeners() {
// Automatically accept all room invitations // Automatically accept all room invitations
return this.client.on("RoomMember.membership", (event, member) => { this.client.on("RoomMember.membership", async (event, member) => {
if ( if (
member.membership === "invite" && member.membership === "invite" &&
member.userId === process.env.BOT_USERID && member.userId === this.config.BOT_USERID &&
!this.joinedRooms.includes(member.roomId) !this.joinedRooms.includes(member.roomId)
) { ) {
logger.log("info", "Auto-joining room " + member.roomId); try {
this.client logger.log("info", "Auto-joining room " + member.roomId);
.joinRoom(member.roomId) const room = await this.client.joinRoom(member.roomId)
.then(room => { this.sendTextMessage(
this.sendTextMessage( this.config.FACILITATOR_ROOM_ID,
process.env.FACILITATOR_ROOM_ID, `A support seeker requested a chat (Room ID: ${member.roomId})`
`A support seeker requested a chat (Room ID: ${member.roomId})` );
); this.inviteFacilitators(member.roomId)
}) } catch(err) {
.then(() => this.inviteFacilitators(member.roomId)) logger.log("error", err);
.catch(err => { }
logger.log("error", err);
});
} }
// When a facilitator joins a support session, make them a moderator // When a facilitator joins a support session, make them a moderator
// revoke the other invitations // revoke the other invitations
if ( if (
member.membership === "join" && member.membership === "join" &&
member.userId !== process.env.BOT_USERID && member.userId !== this.config.BOT_USERID &&
this.awaitingFacilitator[member.roomId] this.awaitingFacilitator[member.roomId]
) { ) {
this.activeChatrooms[member.roomId] = { this.activeChatrooms[member.roomId] = {
@ -383,7 +334,7 @@ class OcrccBot {
getContent: () => { getContent: () => {
return { return {
users: { users: {
[process.env.BOT_USERID]: 100, [this.config.BOT_USERID]: 100,
[member.userId]: 50 [member.userId]: 50
} }
}; };
@ -395,11 +346,11 @@ class OcrccBot {
`${member.name} has joined the chat.` `${member.name} has joined the chat.`
); );
this.sendTextMessage( this.sendTextMessage(
process.env.FACILITATOR_ROOM_ID, this.config.FACILITATOR_ROOM_ID,
`${member.name} joined the chat (Room ID: ${member.roomId})` `${member.name} joined the chat (Room ID: ${member.roomId})`
); );
this.uninviteFacilitators(member.roomId); this.uninviteFacilitators(member.roomId);
if (process.env.CAPTURE_TRANSCRIPTS) { if (this.config.CAPTURE_TRANSCRIPTS) {
const currentDate = new Date(); const currentDate = new Date();
const dateOpts = { const dateOpts = {
year: "numeric", year: "numeric",
@ -418,7 +369,7 @@ class OcrccBot {
if ( if (
member.membership === "leave" && member.membership === "leave" &&
member.userId !== process.env.BOT_USERID && member.userId !== this.config.BOT_USERID &&
this.activeChatrooms[member.roomId] && this.activeChatrooms[member.roomId] &&
member.userId === this.activeChatrooms[member.roomId].facilitator member.userId === this.activeChatrooms[member.roomId].facilitator
) { ) {
@ -426,6 +377,14 @@ class OcrccBot {
member.roomId, member.roomId,
`${member.name} has left the chat.` `${member.name} has left the chat.`
); );
const room = this.client.getRoom(member.roomId)
const memberCount = room.getJoinedMemberCount()
if (memberCount === 1) {
logger.log("info", `LEAVING EMPTY ROOM ==> ${member.roomId}`);
this.client.leave(event.roomId)
}
} }
}); });
} }
@ -448,54 +407,38 @@ class OcrccBot {
}); });
} }
start() { async start() {
const localStorage = this.createLocalStorage(); const localStorage = this.createLocalStorage();
this.client try {
.login("m.login.password", { const auth = {
user: process.env.BOT_USERNAME, user: this.config.BOT_USERNAME,
password: process.env.BOT_PASSWORD, password: this.config.BOT_PASSWORD,
initial_device_display_name: process.env.BOT_DISPLAY_NAME initial_device_display_name: this.config.BOT_DISPLAY_NAME
}) }
.then(data => { const account = await this.client.login("m.login.password", auth)
const accessToken = data.access_token; logger.log("info", `ACCOUNT ==> ${JSON.stringify(account)}`);
const deviceId = data.device_id;
logger.log("info", `LOGIN DATA ==> ${JSON.stringify(data)}`);
// create new client with full options let opts = {
baseUrl: this.config.MATRIX_SERVER_URL,
accessToken: account.access_token,
userId: this.config.BOT_USERID,
deviceId: account.device_id,
sessionStore: new matrix.WebStorageSessionStore(localStorage)
};
let opts = { this.client = matrix.createClient(opts);
baseUrl: process.env.MATRIX_SERVER_URL, await this.deleteOldDevices()
accessToken: accessToken, await this.leaveOldRooms();
userId: process.env.BOT_USERID, await this.client.initCrypto()
deviceId: deviceId,
sessionStore: new matrix.WebStorageSessionStore(localStorage)
};
this.client = matrix.createClient(opts); this.setMembershipListeners();
}) this.setMessageListeners();
.catch(err => { this.client.startClient({ initialSyncLimit: 0 })
logger.log("error", `ERROR WITH LOGIN: ${err}`); } catch(err) {
}) this.handleBotCrash(undefined, err);
.then(() => { logger.log("error", `ERROR INITIALIZING CLIENT: ${err}`);
this.deleteOldDevices(); }
})
.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(() => {
this.setMembershipListeners();
this.setMessageListeners();
})
.then(() => this.client.startClient({ initialSyncLimit: 0 }))
.catch(err => {
this.handleBotCrash(undefined, err);
logger.log("error", `ERROR INITIALIZING CLIENT: ${err}`);
});
} }
} }

View File

@ -1,6 +1,39 @@
require('dotenv').config() 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_GROUP_ID,
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_GROUP_ID,
FACILITATOR_ROOM_ID,
CHAT_OFFLINE_MESSAGE,
CAPTURE_TRANSCRIPTS
}
import OcrccBot from './bot' import OcrccBot from './bot'
const bot = new OcrccBot(); const bot = new OcrccBot(botConfig);
bot.start(); bot.start();

View File

@ -1,5 +0,0 @@
@help-bot:rhok.space [17:39:36]: Facilitator Demo Account has joined the chat.
@ocrcc-facilitator-demo:rhok.space [17:39:48]: heyooo
@help-bot:rhok.space [17:41:13]: Bleep bloop
@help-bot:rhok.space [17:41:21]: 20 Mar 2020 - 17:39:35 - !HyOQxerRxiwlUBolJA:rhok.space.txt
@95326bf0-cd5e-45d7-be64-cb413d37d929:rhok.space [17:42:01]: hi

912
yarn.lock

File diff suppressed because it is too large Load Diff