refactor into a bot class
This commit is contained in:
parent
5570f79e69
commit
febd3feb61
8
.babelrc
Normal file
8
.babelrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"dynamic-import-node"
|
||||||
|
]
|
||||||
|
}
|
70
package.json
70
package.json
@ -4,11 +4,7 @@
|
|||||||
"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": {
|
||||||
"build": "NODE_ENV=production webpack-cli --mode production",
|
"start": "nodemon --exec babel-node src/index.js"
|
||||||
"start": "webpack-dev-server",
|
|
||||||
"develop": "webpack --mode development --watch",
|
|
||||||
"test": "jest",
|
|
||||||
"lint": "./node_modules/.bin/eslint ."
|
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -22,64 +18,10 @@
|
|||||||
"winston": "^3.2.1"
|
"winston": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.7.7",
|
"@babel/core": "^7.8.4",
|
||||||
"@babel/plugin-proposal-class-properties": "7.7.4",
|
"@babel/node": "^7.8.4",
|
||||||
"@babel/plugin-proposal-decorators": "7.7.4",
|
"@babel/preset-env": "^7.8.4",
|
||||||
"@babel/plugin-proposal-export-namespace-from": "7.7.4",
|
"babel-plugin-dynamic-import-node": "^2.3.0",
|
||||||
"@babel/plugin-proposal-function-sent": "7.7.4",
|
"nodemon": "^2.0.2"
|
||||||
"@babel/plugin-proposal-json-strings": "7.7.4",
|
|
||||||
"@babel/plugin-proposal-numeric-separator": "7.7.4",
|
|
||||||
"@babel/plugin-proposal-throw-expressions": "7.7.4",
|
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.7.4",
|
|
||||||
"@babel/plugin-syntax-import-meta": "7.7.4",
|
|
||||||
"@storybook/addon-actions": "5.2.8",
|
|
||||||
"@storybook/addon-links": "5.2.8",
|
|
||||||
"@storybook/addons": "5.2.8",
|
|
||||||
"@storybook/react": "5.2.8",
|
|
||||||
"babel-core": "7.0.0-bridge.0",
|
|
||||||
"babel-eslint": "10.0.3",
|
|
||||||
"babel-jest": "24.9.0",
|
|
||||||
"babel-loader": "8.0.6",
|
|
||||||
"babel-preset-airbnb": "4.4.0",
|
|
||||||
"clean-webpack-plugin": "3.0.0",
|
|
||||||
"copy-webpack-plugin": "5.1.1",
|
|
||||||
"enzyme": "3.11.0",
|
|
||||||
"enzyme-adapter-react-16": "1.15.2",
|
|
||||||
"enzyme-to-json": "3.4.3",
|
|
||||||
"eslint": "6.8.0",
|
|
||||||
"eslint-config-airbnb": "18.0.1",
|
|
||||||
"eslint-import-resolver-webpack": "0.12.0",
|
|
||||||
"eslint-loader": "3.0.3",
|
|
||||||
"eslint-plugin-import": "2.19.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
|
||||||
"eslint-plugin-react": "7.17.0",
|
|
||||||
"jest": "24.9.0",
|
|
||||||
"jest-cli": "24.9.0",
|
|
||||||
"webpack": "4.41.5",
|
|
||||||
"webpack-cli": "3.3.10",
|
|
||||||
"webpack-dev-server": "3.10.1",
|
|
||||||
"webpack-obfuscator": "0.22.0",
|
|
||||||
"webpack-serve": "3.2.0"
|
|
||||||
},
|
|
||||||
"babel": {
|
|
||||||
"presets": [
|
|
||||||
"airbnb"
|
|
||||||
],
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
203
src/index.js
203
src/index.js
@ -1,73 +1,38 @@
|
|||||||
const path = require("path")
|
import * as fs from 'fs'
|
||||||
const fs = require("fs")
|
import * as os from 'os'
|
||||||
const os = require("os")
|
import * as path from 'path'
|
||||||
const util = require("util")
|
import * as util from 'util'
|
||||||
|
import { LocalStorage } from "node-localstorage";
|
||||||
|
import { uuid } from "uuidv4"
|
||||||
|
import config from 'config';
|
||||||
|
|
||||||
const config = require('config');
|
|
||||||
const winston = require('winston');
|
|
||||||
const uuid = require('uuidv4').uuid;
|
|
||||||
const LocalStorage = require('node-localstorage').LocalStorage;
|
|
||||||
global.Olm = require('olm');
|
global.Olm = require('olm');
|
||||||
const matrix = require('matrix-js-sdk');
|
|
||||||
|
import * as matrix from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import logger from './logger'
|
||||||
|
|
||||||
const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" };
|
const ENCRYPTION_CONFIG = { "algorithm": "m.megolm.v1.aes-sha2" };
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
class OcrccBot {
|
||||||
level: 'info',
|
constructor() {
|
||||||
format: winston.format.json(),
|
this.awaitingAgreement = {}
|
||||||
defaultMeta: { service: 'user-service' },
|
this.awaitingFacilitator = {}
|
||||||
transports: [
|
this.client = matrix.createClient(config.get('homeserverUrl'))
|
||||||
//
|
|
||||||
// - Write all logs with level `error` and below to `error.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: 'combined.log' })
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// If we're not in production then log to the `console` with the format:
|
|
||||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
|
||||||
//
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
logger.add(new winston.transports.Console({
|
|
||||||
format: winston.format.simple()
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeserverUrl = 'https://matrix.rhok.space'
|
createLocalStorage() {
|
||||||
const accessToken = 'MDAxOGxvY2F0aW9uIHJob2suc3BhY2UKMDAxM2lkZW50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI3Y2lkIHVzZXJfaWQgPSBAaGVscC1ib3Q6cmhvay5zcGFjZQowMDE2Y2lkIHR5cGUgPSBhY2Nlc3MKMDAyMWNpZCBub25jZSA9IGZBOCsjWWQ4MF9LeTssaF8KMDAyZnNpZ25hdHVyZSA370YUvuoVD3r08AwdgGV9sE0aNWBRTrKvB1me8Bm0tQo'
|
const storageLoc = `matrix-chatbot-${config.get('username')}`
|
||||||
const botName = 'Help Bot'
|
|
||||||
const username = 'help-bot'
|
|
||||||
const password = 'ocrccdemo'
|
|
||||||
const userId = "@help-bot:rhok.space"
|
|
||||||
const waitingRoomId = '!pYVVPyFKacZeKZbWyz:rhok.space'
|
|
||||||
const introMessage = 'This chat application does not collect any of your personal data or any data from your use of this service.'
|
|
||||||
const termsUrl = 'https://tosdr.org/'
|
|
||||||
const agreementMessage = 'Do you want to continue?'
|
|
||||||
const confirmationMessage = 'A faciltator will be with you soon.'
|
|
||||||
const exitMessage = 'That chat was not started. You can close this chatbox.'
|
|
||||||
|
|
||||||
let awaitingAgreement = {}
|
|
||||||
let awaitingFacilitator = {}
|
|
||||||
|
|
||||||
let client = matrix.createClient(homeserverUrl)
|
|
||||||
|
|
||||||
let localStorage = global.localStorage;
|
|
||||||
if (typeof localStorage === "undefined" || localStorage === null) {
|
|
||||||
const storageLoc = `matrix-chatbot-${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))
|
||||||
localStorage = new LocalStorage(localStoragePath);
|
return new LocalStorage(localStoragePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
let deviceId = localStorage.getItem('deviceId')
|
sendMessage(roomId, msgText) {
|
||||||
|
return this.client.sendTextMessage(roomId, msgText)
|
||||||
const sendMessage = (roomId, msgText) => {
|
|
||||||
return client.sendTextMessage(roomId, msgText)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
logger.log('info', "Message sent")
|
logger.log('info', "Message sent")
|
||||||
logger.log('info', res)
|
logger.log('info', res)
|
||||||
@ -77,10 +42,10 @@ const sendMessage = (roomId, msgText) => {
|
|||||||
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) => {
|
||||||
client.setDeviceVerified(userId, deviceId, true);
|
this.client.setDeviceVerified(userId, deviceId, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return sendMessage(roomId, msgText)
|
return this.sendMessage(roomId, msgText)
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.log('error', "Error sending message");
|
logger.log('error', "Error sending message");
|
||||||
@ -90,33 +55,55 @@ const sendMessage = (roomId, msgText) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteFacilitators = (roomId) => {
|
sendHtmlMessage(roomId, msgText, msgHtml) {
|
||||||
awaitingFacilitator[roomId] = true
|
return this.client.sendHtmlMessage(roomId, msgText, msgHtml)
|
||||||
client.getJoinedRoomMembers(waitingRoomId)
|
.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) {
|
||||||
|
this.awaitingFacilitator[roomId] = true
|
||||||
|
this.client.getJoinedRoomMembers(config.get('waitingRoomId'))
|
||||||
.then((members) => {
|
.then((members) => {
|
||||||
logger.log("info", "MEMBERS")
|
|
||||||
logger.log("info", members)
|
|
||||||
Object.keys(members["joined"]).forEach((member) => {
|
Object.keys(members["joined"]).forEach((member) => {
|
||||||
if (member !== userId)
|
if (member !== config.get('userId'))
|
||||||
client.invite(roomId, member)
|
this.client.invite(roomId, member)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// const notif = `There is a support seeker waiting. Go to https://riot.im/app/#/room/${roomId} to respond.`
|
// const notif = `There is a support seeker waiting. Go to https://riot.im/app/#/room/${roomId} to respond.`
|
||||||
// sendMessage(waitingRoomId, notif)
|
// sendMessage(waitingRoomId, notif)
|
||||||
}
|
}
|
||||||
|
|
||||||
const kickFacilitators = (roomId) => {
|
uninviteFacilitators(roomId) {
|
||||||
awaitingFacilitator[roomId] = false
|
this.awaitingFacilitator[roomId] = false
|
||||||
client.getJoinedRoomMembers(waitingRoomId)
|
this.client.getJoinedRoomMembers(config.get('waitingRoomId'))
|
||||||
.then((allFacilitators) => {
|
.then((allFacilitators) => {
|
||||||
client.getJoinedRoomMembers(roomId)
|
this.client.getJoinedRoomMembers(roomId)
|
||||||
.then((roomMembers) => {
|
.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)
|
||||||
client.kick(roomId, f, "A facilitator has already joined this chat.")
|
this.client.kick(roomId, f, "A facilitator has already joined this chat.")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.log("info", "Kick success")
|
logger.log("info", "Kick success")
|
||||||
})
|
})
|
||||||
@ -129,10 +116,14 @@ const kickFacilitators = (roomId) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
client.login('m.login.password', {
|
start() {
|
||||||
user: username,
|
const localStorage = this.createLocalStorage()
|
||||||
password: password,
|
let deviceId = localStorage.getItem('deviceId')
|
||||||
initial_device_display_name: botName,
|
|
||||||
|
this.client.login('m.login.password', {
|
||||||
|
user: config.get('username'),
|
||||||
|
password: config.get('password'),
|
||||||
|
initial_device_display_name: config.get('botName'),
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -140,67 +131,75 @@ client.login('m.login.password', {
|
|||||||
const deviceId = data.device_id
|
const deviceId = data.device_id
|
||||||
|
|
||||||
localStorage.setItem('deviceId', data.device_id)
|
localStorage.setItem('deviceId', data.device_id)
|
||||||
|
|
||||||
// create new client with full options
|
// create new client with full options
|
||||||
|
|
||||||
let opts = {
|
let opts = {
|
||||||
baseUrl: homeserverUrl,
|
baseUrl: config.get('homeserverUrl'),
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
userId: userId,
|
userId: config.get('userId'),
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
sessionStore: new matrix.WebStorageSessionStore(localStorage),
|
sessionStore: new matrix.WebStorageSessionStore(localStorage),
|
||||||
}
|
}
|
||||||
|
|
||||||
client = matrix.createClient(opts)
|
this.client = matrix.createClient(opts)
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.log('error', `Login error: ${err}`)
|
logger.log('error', `Login error: ${err}`)
|
||||||
})
|
})
|
||||||
.then(() => client.initCrypto())
|
.then(() => this.client.initCrypto())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
// Automatically join all room invitations
|
// Automatically accept all room invitations
|
||||||
client.on("RoomMember.membership", (event, member) => {
|
// On joining a room, send the intro messages and wait for agreement to continue
|
||||||
if (member.membership === "invite" && member.userId === userId) {
|
this.client.on("RoomMember.membership", (event, member) => {
|
||||||
|
if (member.membership === "invite" && member.userId === config.get('userId')) {
|
||||||
logger.log("info", "Auto-joining room " + member.roomId)
|
logger.log("info", "Auto-joining room " + member.roomId)
|
||||||
client.joinRoom(member.roomId)
|
this.client.joinRoom(member.roomId)
|
||||||
.then(() => client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG))
|
.then(() => this.client.setRoomEncryption(member.roomId, ENCRYPTION_CONFIG))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (member.roomId !== waitingRoomId) {
|
if (member.roomId !== config.get('waitingRoomId')) {
|
||||||
sendMessage(member.roomId, introMessage)
|
this.sendMessage(member.roomId, config.get('introMessage'))
|
||||||
.then(() => sendMessage(member.roomId, `Please read the terms and conditions at ${termsUrl}`))
|
.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(() => sendMessage(member.roomId, agreementMessage))
|
.then(() => this.sendMessage(member.roomId, config.get('agreementMessage')))
|
||||||
.then(() => awaitingAgreement[member.roomId] = true)
|
.then(() => this.awaitingAgreement[member.roomId] = true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("info", "Membership event: " + JSON.stringify(member))
|
// When the first facilitator joins a support session, uninvite the other facilitators
|
||||||
logger.log("info", "Awaiting facilitator: " + awaitingFacilitator[member.roomId])
|
if (member.membership === 'join' && this.awaitingFacilitator[member.roomId]) {
|
||||||
|
this.uninviteFacilitators(member.roomId)
|
||||||
if (member.membership === 'join' && awaitingFacilitator[member.roomId]) {
|
|
||||||
kickFacilitators(member.roomId)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('Event.decrypted', (event) => {
|
// Listen for incoming messages
|
||||||
|
this.client.on('Event.decrypted', (event) => {
|
||||||
if (event.getType() === 'm.room.message') {
|
if (event.getType() === 'm.room.message') {
|
||||||
const roomId = event.getRoomId()
|
const roomId = event.getRoomId()
|
||||||
const sender = event.getSender()
|
const sender = event.getSender()
|
||||||
const content = event.getContent()
|
const content = event.getContent()
|
||||||
const body = content.body
|
const body = content.body
|
||||||
|
|
||||||
if (sender !== userId && awaitingAgreement[roomId]) {
|
// 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')) {
|
if (body.toLowerCase().startsWith('yes')) {
|
||||||
sendMessage(roomId, confirmationMessage)
|
this.sendMessage(roomId, config.get('confirmationMessage'))
|
||||||
inviteFacilitators(roomId)
|
this.inviteFacilitators(roomId)
|
||||||
awaitingAgreement[roomId] = false
|
this.awaitingAgreement[roomId] = false
|
||||||
} else {
|
} else {
|
||||||
sendMessage(roomId, exitMessage)
|
this.sendMessage(roomId, config.get('exitMessage'))
|
||||||
awaitingAgreement[roomId] = false
|
this.awaitingAgreement[roomId] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => client.startClient())
|
.finally(() => this.client.startClient())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = new OcrccBot();
|
||||||
|
bot.start()
|
||||||
|
27
src/logger.js
Normal file
27
src/logger.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import winston from 'winston'
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: 'info',
|
||||||
|
format: winston.format.json(),
|
||||||
|
defaultMeta: { service: 'user-service' },
|
||||||
|
transports: [
|
||||||
|
//
|
||||||
|
// - Write all logs with level `error` and below to `error.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: 'combined.log' })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// If we're not in production then log to the `console` with the format:
|
||||||
|
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||||
|
//
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.add(new winston.transports.Console({
|
||||||
|
format: winston.format.simple()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default logger;
|
Loading…
Reference in New Issue
Block a user