add test suite
This commit is contained in:
parent
7b2da6b351
commit
434147c483
175
__mocks__/matrix-js-sdk.js
Normal file
175
__mocks__/matrix-js-sdk.js
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
export const mockRegisterRequest = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((params) => {
|
||||||
|
if (!params.auth) {
|
||||||
|
return Promise.reject({
|
||||||
|
data: { session: "session_id_1234" }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
device_id: 'device_id_1234',
|
||||||
|
access_token: 'token_1234',
|
||||||
|
user_id: 'user_id_1234',
|
||||||
|
session: "session_id_1234"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockDeleteMultipleDevices = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((params) => {
|
||||||
|
if (!params.auth) {
|
||||||
|
return Promise.reject({
|
||||||
|
data: { session: "session_id_1234" }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
device_id: 'device_id_1234',
|
||||||
|
access_token: 'token_1234',
|
||||||
|
user_id: 'user_id_1234',
|
||||||
|
session: "session_id_1234"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockLeave = jest.fn(() => {
|
||||||
|
return Promise.resolve('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockInitCrypto = jest.fn()
|
||||||
|
|
||||||
|
export const mockStartClient = jest.fn(() => {
|
||||||
|
return Promise.resolve('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockOnce = jest.fn()
|
||||||
|
|
||||||
|
export const mockStopClient = jest.fn(() => {
|
||||||
|
return Promise.resolve('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockClearStores = jest.fn(() => {
|
||||||
|
return Promise.resolve('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockGetRoom = jest.fn()
|
||||||
|
|
||||||
|
export const mockDownloadKeys = jest.fn()
|
||||||
|
|
||||||
|
export const mockSetDeviceVerified = jest.fn()
|
||||||
|
|
||||||
|
export const mockIsCryptoEnabled = jest.fn()
|
||||||
|
|
||||||
|
export const mockCreateRoom = jest.fn().mockReturnValue({ room_id: 'room_id_1234' })
|
||||||
|
|
||||||
|
export const mockSetPowerLevel = jest.fn()
|
||||||
|
|
||||||
|
export const mockSendTextMessage = jest.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.reject({
|
||||||
|
name: "UnknownDeviceError",
|
||||||
|
devices: ['device1', 'device2']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockSetDeviceKnown = jest.fn()
|
||||||
|
|
||||||
|
export const mockInvite = jest.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.reject({
|
||||||
|
name: "M_LIMIT_EXCEEDED",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockKick = jest.fn()
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.reject({
|
||||||
|
name: "M_LIMIT_EXCEEDED",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mockDeactivateAccount = jest.fn(() => {
|
||||||
|
return Promise.resolve('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockOn = jest.fn()
|
||||||
|
|
||||||
|
export const mockGetDevices = jest.fn(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
devices: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockGetDeviceId = jest.fn().mockReturnValue('mockDeviceId');
|
||||||
|
|
||||||
|
export const mockGetJoinedRooms = jest.fn(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
joined_rooms: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockLogin = jest.fn(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
device_id: 'device_id_1234',
|
||||||
|
access_token: 'token_1234',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockGetJoinedRoomMembers = jest.fn(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
joined: ['user_id_1', 'user_id_2']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockGetUser = jest.fn().mockReturnValue({ presence: 'online'});
|
||||||
|
|
||||||
|
export const mockClient = {
|
||||||
|
registerRequest: mockRegisterRequest,
|
||||||
|
initCrypto: mockInitCrypto,
|
||||||
|
startClient: mockStartClient,
|
||||||
|
on: mockOn,
|
||||||
|
once: mockOnce,
|
||||||
|
leave: mockLeave,
|
||||||
|
stopClient: mockStopClient,
|
||||||
|
clearStores: mockClearStores,
|
||||||
|
getRoom: mockGetRoom,
|
||||||
|
downloadKeys: mockDownloadKeys,
|
||||||
|
setDeviceVerified: mockSetDeviceVerified,
|
||||||
|
setDeviceKnown: mockSetDeviceKnown,
|
||||||
|
isCryptoEnabled: mockIsCryptoEnabled,
|
||||||
|
createRoom: mockCreateRoom,
|
||||||
|
setPowerLevel: mockSetPowerLevel,
|
||||||
|
sendTextMessage: mockSendTextMessage,
|
||||||
|
deactivateAccount: mockDeactivateAccount,
|
||||||
|
login: mockLogin,
|
||||||
|
getDevices: mockGetDevices,
|
||||||
|
getDeviceId: mockGetDeviceId,
|
||||||
|
deleteMultipleDevices: mockDeleteMultipleDevices,
|
||||||
|
getJoinedRooms: mockGetJoinedRooms,
|
||||||
|
invite: mockInvite,
|
||||||
|
kick: mockKick,
|
||||||
|
getJoinedRoomMembers: mockGetJoinedRoomMembers,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WebStorageSessionStore = jest.fn()
|
||||||
|
|
||||||
|
export const createClient = jest.fn().mockReturnValue(mockClient)
|
12
package.json
12
package.json
@ -6,11 +6,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"develop": "nodemon --exec babel-node src/index.js",
|
"develop": "nodemon --exec babel-node src/index.js",
|
||||||
"build": "babel src -d dist",
|
"build": "babel src -d dist",
|
||||||
"start": "yarn build && node dist/index.js"
|
"start": "yarn build && node dist/index.js",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"matrix-js-sdk": "^5.0.1",
|
"matrix-js-sdk": "^5.0.1",
|
||||||
"node-localstorage": "^2.1.5",
|
"node-localstorage": "^2.1.5",
|
||||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||||
@ -21,7 +23,11 @@
|
|||||||
"@babel/core": "^7.8.4",
|
"@babel/core": "^7.8.4",
|
||||||
"@babel/node": "^7.8.4",
|
"@babel/node": "^7.8.4",
|
||||||
"@babel/preset-env": "^7.8.4",
|
"@babel/preset-env": "^7.8.4",
|
||||||
|
"babel-jest": "^25.1.0",
|
||||||
"babel-plugin-dynamic-import-node": "^2.3.0",
|
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||||
"nodemon": "^2.0.2"
|
"jest": "^25.1.0",
|
||||||
|
"jest-cli": "^25.1.0",
|
||||||
|
"nodemon": "^2.0.2",
|
||||||
|
"wait-for-expect": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
380
src/bot.js
Normal file
380
src/bot.js
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as util from "util";
|
||||||
|
import { LocalStorage } from "node-localstorage";
|
||||||
|
|
||||||
|
global.Olm = require("olm");
|
||||||
|
|
||||||
|
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.awaitingFacilitator = {};
|
||||||
|
this.client = matrix.createClient(process.env.MATRIX_SERVER_URL);
|
||||||
|
this.joinedRooms = [];
|
||||||
|
this.activeChatrooms = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
createLocalStorage() {
|
||||||
|
const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`;
|
||||||
|
const dir = path.resolve(path.join(os.homedir(), ".local-storage"));
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir);
|
||||||
|
}
|
||||||
|
const localStoragePath = path.resolve(path.join(dir, storageLoc));
|
||||||
|
return new LocalStorage(localStoragePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(roomId, msgText) {
|
||||||
|
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: ${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) {
|
||||||
|
this.awaitingFacilitator[roomId] = true;
|
||||||
|
let chatOffline = true;
|
||||||
|
this.client
|
||||||
|
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
||||||
|
.then(members => {
|
||||||
|
let onlineMembersCount = 0;
|
||||||
|
Object.keys(members["joined"]).forEach(member => {
|
||||||
|
const user = this.client.getUser(member);
|
||||||
|
if (user.presence === "online" && member !== process.env.BOT_USERID) {
|
||||||
|
chatOffline = false;
|
||||||
|
this.inviteUserToRoom(this.client, roomId, member);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (chatOffline) {
|
||||||
|
this.sendMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.handleBotCrash(roomId, err);
|
||||||
|
logger.log("error", `ERROR GETTING ROOM MEMBERS: ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
uninviteFacilitators(roomId) {
|
||||||
|
this.awaitingFacilitator[roomId] = false;
|
||||||
|
this.client
|
||||||
|
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
||||||
|
.then(allFacilitators => {
|
||||||
|
this.client.getJoinedRoomMembers(roomId).then(roomMembers => {
|
||||||
|
const membersIds = Object.keys(roomMembers["joined"]);
|
||||||
|
const facilitatorsIds = Object.keys(allFacilitators["joined"]);
|
||||||
|
facilitatorsIds.forEach(f => {
|
||||||
|
if (!membersIds.includes(f)) {
|
||||||
|
this.kickUserFromRoom(this.client, roomId, f);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOldDevices() {
|
||||||
|
return 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)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMembershipListeners() {
|
||||||
|
// Automatically accept all room invitations
|
||||||
|
return this.client.on("RoomMember.membership", (event, member) => {
|
||||||
|
if (
|
||||||
|
member.membership === "invite" &&
|
||||||
|
member.userId === process.env.BOT_USERID &&
|
||||||
|
!this.joinedRooms.includes(member.roomId)
|
||||||
|
) {
|
||||||
|
logger.log("info", "Auto-joining room " + member.roomId);
|
||||||
|
this.client
|
||||||
|
.joinRoom(member.roomId)
|
||||||
|
.then(room => {
|
||||||
|
this.sendMessage(
|
||||||
|
process.env.FACILITATOR_ROOM_ID,
|
||||||
|
`A support seeker requested a chat (Room ID: ${member.roomId})`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => this.inviteFacilitators(member.roomId))
|
||||||
|
.catch(err => {
|
||||||
|
logger.log("error", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.`
|
||||||
|
);
|
||||||
|
this.sendMessage(
|
||||||
|
process.env.FACILITATOR_ROOM_ID,
|
||||||
|
`${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.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageListeners() {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const localStorage = this.createLocalStorage();
|
||||||
|
|
||||||
|
this.client
|
||||||
|
.login("m.login.password", {
|
||||||
|
user: process.env.BOT_USERNAME,
|
||||||
|
password: process.env.BOT_PASSWORD,
|
||||||
|
initial_device_display_name: process.env.BOT_DISPLAY_NAME
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
const accessToken = data.access_token;
|
||||||
|
const deviceId = data.device_id;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = matrix.createClient(opts);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.log("error", `ERROR WITH LOGIN: ${err}`);
|
||||||
|
})
|
||||||
|
.then(() => 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()
|
||||||
|
|
||||||
|
if (process.env.CAPTURE_TRANSCRIPTS) {
|
||||||
|
this.setMessageListeners()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => this.client.startClient({ initialSyncLimit: 0 }))
|
||||||
|
.catch(err => {
|
||||||
|
this.handleBotCrash(undefined, err);
|
||||||
|
logger.log("error", `ERROR INITIALIZING CLIENT: ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OcrccBot;
|
||||||
|
|
235
src/bot.test.js
Normal file
235
src/bot.test.js
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
import * as path from "path";
|
||||||
|
import * as os from "os";
|
||||||
|
import waitForExpect from 'wait-for-expect'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
WebStorageSessionStore,
|
||||||
|
mockClient,
|
||||||
|
mockRegisterRequest,
|
||||||
|
mockInitCrypto,
|
||||||
|
mockStartClient,
|
||||||
|
mockSetPowerLevel,
|
||||||
|
mockCreateRoom,
|
||||||
|
mockLeave,
|
||||||
|
mockDeactivateAccount,
|
||||||
|
mockStopClient,
|
||||||
|
mockClearStores,
|
||||||
|
mockOn,
|
||||||
|
mockOnce,
|
||||||
|
mockSendTextMessage,
|
||||||
|
mockLogin,
|
||||||
|
mockGetDevices,
|
||||||
|
mockGetDeviceId,
|
||||||
|
mockDeleteMultipleDevices,
|
||||||
|
mockGetJoinedRooms,
|
||||||
|
mockSetDeviceVerified,
|
||||||
|
mockInvite,
|
||||||
|
mockKick,
|
||||||
|
mockGetJoinedRoomMembers,
|
||||||
|
mockGetUser
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import OcrccBot from './bot'
|
||||||
|
|
||||||
|
describe('OcrccBot', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createClient.mockClear()
|
||||||
|
mockInitCrypto.mockClear()
|
||||||
|
mockStartClient.mockClear()
|
||||||
|
mockRegisterRequest.mockClear()
|
||||||
|
mockSetPowerLevel.mockClear()
|
||||||
|
mockCreateRoom.mockClear()
|
||||||
|
mockLeave.mockClear()
|
||||||
|
mockDeactivateAccount.mockClear()
|
||||||
|
mockStopClient.mockClear()
|
||||||
|
mockClearStores.mockClear()
|
||||||
|
mockOnce.mockClear()
|
||||||
|
mockOn.mockClear()
|
||||||
|
mockLogin.mockClear()
|
||||||
|
mockGetDevices.mockClear()
|
||||||
|
mockGetDeviceId.mockClear()
|
||||||
|
mockDeleteMultipleDevices.mockClear()
|
||||||
|
mockGetJoinedRooms.mockClear()
|
||||||
|
mockSetDeviceVerified.mockClear()
|
||||||
|
mockInvite.mockClear()
|
||||||
|
mockKick.mockClear()
|
||||||
|
mockGetJoinedRoomMembers.mockClear()
|
||||||
|
mockGetUser.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('constructor should inititialize matrix client', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
expect(createClient).toHaveBeenCalledWith(process.env.MATRIX_SERVER_URL)
|
||||||
|
expect(bot.joinedRooms).toEqual([])
|
||||||
|
expect(bot.awaitingFacilitator).toEqual({})
|
||||||
|
expect(bot.activeChatrooms).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#createLocalStorage should have correct storage location', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
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 OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const testRoom = 'room_id_1234'
|
||||||
|
const testMsg = 'test message'
|
||||||
|
|
||||||
|
bot.sendMessage(testRoom, testMsg)
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockSetDeviceVerified).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockSendTextMessage).toHaveBeenCalledWith(testRoom, testMsg)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#inviteUserToRoom should add member to room and retry on rate limit error', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.inviteUserToRoom(bot.client, 'room_id_1234', process.env.BOT_USERNAME)
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockInvite).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#kickUserFromRoom should remove member from room and retry on rate limit error', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.kickUserFromRoom(bot.client, 'room_id_1234', process.env.BOT_USERNAME)
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockKick).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#inviteFacilitators should invite all members from Facilitator room', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.inviteFacilitators()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetJoinedRoomMembers).toHaveBeenCalledWith(process.env.FACILITATOR_ROOM_ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetUser).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockInvite).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#uninviteFacilitators should remove all members that have not accepted the invite', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
bot.uninviteFacilitators()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetJoinedRoomMembers).toHaveBeenCalledWith(process.env.FACILITATOR_ROOM_ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetJoinedRoomMembers).toHaveBeenCalledWith('room_id_1234')
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockKick).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#handleBotCrash should notify rooms', () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#writeToTranscript should parse event and write to transcript file', () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#deleteOldDevices should delete old sessions', () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO test listeners for membership events and message events
|
||||||
|
|
||||||
|
test('#start should start bot and set up listeners', () => {
|
||||||
|
const bot = new OcrccBot()
|
||||||
|
bot.start()
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockLogin).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(WebStorageSessionStore).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(createClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetDevices).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetDeviceId).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockDeleteMultipleDevices).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockInitCrypto).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockGetJoinedRooms).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockOn).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
waitForExpect(() => {
|
||||||
|
expect(mockStartClient).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
367
src/index.js
367
src/index.js
@ -1,369 +1,6 @@
|
|||||||
import * as fs from "fs";
|
require('dotenv').config()
|
||||||
import * as os from "os";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as util from "util";
|
|
||||||
import { LocalStorage } from "node-localstorage";
|
|
||||||
|
|
||||||
global.Olm = require("olm");
|
import OcrccBot from './bot'
|
||||||
|
|
||||||
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.awaitingFacilitator = {};
|
|
||||||
this.client = matrix.createClient(process.env.MATRIX_SERVER_URL);
|
|
||||||
this.joinedRooms = [];
|
|
||||||
this.activeChatrooms = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
createLocalStorage() {
|
|
||||||
const storageLoc = `matrix-chatbot-${process.env.BOT_USERNAME}`;
|
|
||||||
const dir = path.resolve(path.join(os.homedir(), ".local-storage"));
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir);
|
|
||||||
}
|
|
||||||
const localStoragePath = path.resolve(path.join(dir, storageLoc));
|
|
||||||
return new LocalStorage(localStoragePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(roomId, msgText) {
|
|
||||||
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: ${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) {
|
|
||||||
this.awaitingFacilitator[roomId] = true;
|
|
||||||
let chatOffline = true;
|
|
||||||
this.client
|
|
||||||
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
|
||||||
.then(members => {
|
|
||||||
let onlineMembersCount = 0;
|
|
||||||
Object.keys(members["joined"]).forEach(member => {
|
|
||||||
const user = this.client.getUser(member);
|
|
||||||
if (user.presence === "online" && member !== process.env.BOT_USERID) {
|
|
||||||
chatOffline = false;
|
|
||||||
this.inviteUserToRoom(this.client, roomId, member);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (chatOffline) {
|
|
||||||
this.sendMessage(roomId, process.env.CHAT_OFFLINE_MESSAGE);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
this.handleBotCrash(roomId, err);
|
|
||||||
logger.log("error", `ERROR GETTING ROOM MEMBERS: ${err}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uninviteFacilitators(roomId) {
|
|
||||||
this.awaitingFacilitator[roomId] = false;
|
|
||||||
this.client
|
|
||||||
.getJoinedRoomMembers(process.env.FACILITATOR_ROOM_ID)
|
|
||||||
.then(allFacilitators => {
|
|
||||||
this.client.getJoinedRoomMembers(roomId).then(roomMembers => {
|
|
||||||
const membersIds = Object.keys(roomMembers["joined"]);
|
|
||||||
const facilitatorsIds = Object.keys(allFacilitators["joined"]);
|
|
||||||
facilitatorsIds.forEach(f => {
|
|
||||||
if (!membersIds.includes(f)) {
|
|
||||||
this.kickUserFromRoom(this.client, roomId, f);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.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() {
|
|
||||||
const localStorage = this.createLocalStorage();
|
|
||||||
|
|
||||||
this.client
|
|
||||||
.login("m.login.password", {
|
|
||||||
user: process.env.BOT_USERNAME,
|
|
||||||
password: process.env.BOT_PASSWORD,
|
|
||||||
initial_device_display_name: process.env.BOT_DISPLAY_NAME
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
const accessToken = data.access_token;
|
|
||||||
const deviceId = data.device_id;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
};
|
|
||||||
|
|
||||||
this.client = matrix.createClient(opts);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.log("error", `ERROR WITH LOGIN: ${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"];
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
// Automatically accept all room invitations
|
|
||||||
this.client.on("RoomMember.membership", (event, member) => {
|
|
||||||
if (
|
|
||||||
member.membership === "invite" &&
|
|
||||||
member.userId === process.env.BOT_USERID &&
|
|
||||||
!this.joinedRooms.includes(member.roomId)
|
|
||||||
) {
|
|
||||||
logger.log("info", "Auto-joining room " + member.roomId);
|
|
||||||
this.client
|
|
||||||
.joinRoom(member.roomId)
|
|
||||||
.then(room => {
|
|
||||||
this.sendMessage(
|
|
||||||
process.env.FACILITATOR_ROOM_ID,
|
|
||||||
`A support seeker requested a chat (Room ID: ${member.roomId})`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(() => this.inviteFacilitators(member.roomId))
|
|
||||||
.catch(err => {
|
|
||||||
logger.log("error", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.`
|
|
||||||
);
|
|
||||||
this.sendMessage(
|
|
||||||
process.env.FACILITATOR_ROOM_ID,
|
|
||||||
`${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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => this.client.startClient({ initialSyncLimit: 0 }))
|
|
||||||
.catch(err => {
|
|
||||||
this.handleBotCrash(undefined, err);
|
|
||||||
logger.log("error", `ERROR INITIALIZING CLIENT: ${err}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bot = new OcrccBot();
|
const bot = new OcrccBot();
|
||||||
bot.start();
|
bot.start();
|
||||||
|
Loading…
Reference in New Issue
Block a user