UNPKG

wickrio-bot-api

Version:
969 lines (894 loc) 26.6 kB
const WickrIOAddon = require('wickrio_addon') const WickrIOConfigure = require('./WickrIOConfigure') const WickrUser = require('./WickrUser') const WickrAdmin = require('./WickrAdmin') const WickrLogger = require('./WickrLogger') const MessageService = require('./services/message') const fs = require('fs') const APIService = require('./services/api') const path = require('path') const util = require('util') let encryptor let encryptorDefined = false const logger = new WickrLogger().logger class WickrIOBot { constructor(debugOn) { this.wickrIOAPI = new WickrIOAddon.WickrIOAddon(debugOn); this.wickrUsers = [] // wickrusers populate on load data which happens on start, or on addUser() this.listenFlag = false this.adminOnly = false this.myAdmins = null // admins dont populate until start this.debug = debugOn // console.log = function () { // logger.info(util.format.apply(null, arguments)) // } // console.error = function () { // logger.error(util.format.apply(null, arguments)) // } } messageService({ rawMessage, adminDMonly = false, testOnly = false }) { return new MessageService({ rawMessage, admins: this.myAdmins, adminOnly: this.adminOnly, wickrUsers: this.wickrUsers, adminDMonly, wickrAPI: this.wickrIOAPI, testOnly, }) } apiService() { return new APIService({ WickrIOAPI: this.wickrIOAPI, }) } async provision({ status, setAdminOnly = false, attachLifeMinutes = '0', doreceive = 'true', duration = '0', readreceipt = 'true', cleardb = 'true', contactbackup = 'false', convobackup = 'false', verifyusers = { encryption: false, value: 'automatic' }, }) { if (setAdminOnly === true || setAdminOnly === 'true') { setAdminOnly = 'true' } if (!status) { this.exitHandler(null, { exit: true, reason: 'Client not able to start', }) } this.setAdminOnly(setAdminOnly) // set the verification mode to true // let verifyUsersMode // const VERIFY_USERS = JSON.parse(process.env.tokens).VERIFY_USERS if (verifyusers.encrypted) { verifyusers.value = await this.wickrIOAPI.cmdDecryptString(verifyusers.value) } // else { // verifyUsersMode = VERIFY_USERS.value // } this.setVerificationMode(verifyusers.value) await this.wickrIOAPI.cmdSetControl('attachLifeMinutes', attachLifeMinutes.toString()) await this.wickrIOAPI.cmdSetControl('doreceive', doreceive.toString()) await this.wickrIOAPI.cmdSetControl('duration', duration.toString()) await this.wickrIOAPI.cmdSetControl('readreceipt', readreceipt.toString()) await this.wickrIOAPI.cmdSetControl('cleardb', cleardb.toString()) // ? await this.wickrIOAPI.cmdSetControl('contactbackup', contactbackup.toString()) // ? await this.wickrIOAPI.cmdSetControl('convobackup', convobackup.toString()) // ? } /* * Return the WickrIO addon API */ getWickrIOAddon() { return this.wickrIOAPI } /* * Set this client to handle only commands from admin users */ setAdminOnly(setting) { this.adminOnly = setting } getAdminHelp(helpString) { return this.myAdmins.getHelp(helpString) } setVerificationMode(mode) { this.myAdmins.setVerifyMode(mode) } /* * WickrIO API functions used: clientInit() and isConnected() */ async start(client_username) { const myLocalAdmins = new WickrAdmin(this.wickrIOAPI) console.log('starting bot') this.myAdmins = myLocalAdmins const clientinitPromise = client_username => new Promise((resolve, reject) => { const status = this.wickrIOAPI.clientInit(client_username) resolve(status) }) const clientconnectionPromise = () => new Promise(async (resolve, reject) => { console.log('Checking for client connectionn...') let connected = false do { console.log('calling isConnected...') connected = await this.wickrIOAPI.isConnected(10) console.log('isConnected:', connected) } while (connected != true) console.log('isConnected: finally we are connected') let cState do { cState = await this.wickrIOAPI.getClientState() // this.wickrIOAPI.getClientState().then(result =>{ // cState = result // }); if (cState === undefined) { console.log('cState === undefined') } console.log('isConnected: client state is', cState) if (cState != 'RUNNING') sleep(5000) } while (cState != 'RUNNING') resolve(connected) }) const processAdminUsers = async connected => { /* * Process the admin users */ const processes = JSON.parse(fs.readFileSync('processes.json')) if (process.env.tokens !== undefined) { const tokens = JSON.parse(process.env.tokens) let administrators if ( (!tokens.ADMINISTRATORS_CHOICE || (tokens.ADMINISTRATORS_CHOICE && tokens.ADMINISTRATORS_CHOICE.value === 'yes')) && tokens.ADMINISTRATORS && tokens.ADMINISTRATORS.value ) { if (tokens.ADMINISTRATORS.encrypted) { administrators = await this.wickrIOAPI.cmdDecryptString( tokens.ADMINISTRATORS.value ) } else { administrators = tokens.ADMINISTRATORS.value } administrators = administrators.split(/[ ,]+/) // Make sure there are no white spaces on the whitelisted users for (let i = 0; i < administrators.length; i++) { const administrator = administrators[i].trim() const admin = myLocalAdmins.addAdmin(administrator) } } } const settings = JSON.parse(fs.readFileSync('package.json')) // Check if bot supports a user database if (!settings.database) { return true } if (connected) { const encrypted = await this.encryptEnv() const loaded = await this.loadData() return true } else { console.log('not connected, not processing admin users') return false } } const client = await clientinitPromise(client_username) if (client) { console.log({ client }) const connection = await clientconnectionPromise() console.log({ connection }) if (connection) { return processAdminUsers(connection) } } } /* * This start function is specific to the testing scripts */ async startForTesting(client_username) { const myLocalAdmins = new WickrAdmin(this.wickrIOAPI) console.log('test starting bot') this.myAdmins = myLocalAdmins } /* * WickrIO API functions used: cmdStartAsyncRecvMessages */ async startListening(callback) { try { const ref = this return new Promise(async function (resolve, reject) { const start = await ref.wickrIOAPI.cmdStartAsyncRecvMessages(callback) if (start === 'Success') resolve(start) else reject(start) }) .then(function (start) { ref.listenFlag = true console.log('Bot message listener set successfully!') return true }) .catch(error => { console.log('Bot message listener failed to set:', error) return false }) } catch (err) { console.error(err) } } /* * WickrIO API functions used: closeClient() and cmdStopAsyncRecvMessages() */ async close() { try { const ref = this const settings = JSON.parse(fs.readFileSync('package.json')) // Checks if bot supports a user database saving feature if (settings.database) { const saved = await this.saveData() } return new Promise(async function (resolve, reject) { let stopMessaging = 'not needed' if (ref.listenFlag === true) stopMessaging = await this.wickrIOAPI.cmdStopAsyncRecvMessages() resolve(stopMessaging) }) .then(function (stopMessaging) { if (stopMessaging === 'Success') { console.log('Async message receiving stopped!') } console.log('Shutting bot down...') return new Promise(function (resolve, reject) { if (this?.wickrIOAPI) { const closed = this.wickrIOAPI.closeClient() resolve(closed) } else { resolve(true) } }) .then(function (closed) { console.log(closed) console.log('Bot shut down successfully!') return true }) .catch(error => { console.error(error) }) }) .catch(error => { console.error(error) }) } catch (err) { console.error(err) return false } } /* * WickrIO API functions used: cmdEncryptString() */ async encryptEnv() { if (process.env.tokens === undefined) { return true } try { const processes = JSON.parse(fs.readFileSync('processes.json')) const tokens = JSON.parse(process.env.tokens) // Create an encryptor: let key // if the encryption choice value is there and is 'no' then return if ( tokens.DATABASE_ENCRYPTION_CHOICE === undefined || tokens.DATABASE_ENCRYPTION_CHOICE.value !== 'yes' ) { console.log('WARNING: Configurations are not encrypted') return true } if (tokens.DATABASE_ENCRYPTION_KEY.encrypted) { key = await this.wickrIOAPI.cmdDecryptString(tokens.DATABASE_ENCRYPTION_KEY.value) } else { key = tokens.DATABASE_ENCRYPTION_KEY.value } if (key.length < 16) { console.log( 'WARNING: ENCRYPTION_KEY value is too short, must be at least 16 characters long' ) encryptorDefined = false return true } encryptor = require('simple-encryptor')(key) encryptorDefined = true for (const i in tokens) { if (i === 'BOT_USERNAME' || i === 'WICKRIO_BOT_NAME') continue if (!tokens[i].encrypted) { tokens[i].value = await this.wickrIOAPI.cmdEncryptString(tokens[i].value) tokens[i].encrypted = true } } processes.apps[0].env.tokens = tokens const ps = fs.writeFileSync( './processes.json', JSON.stringify(processes, null, 2) ) console.log('Bot tokens encrypted successfully!') return true } catch (err) { console.error('Unable to encrypt Bot Tokens:', err) return false } } /* * Loads and decrypts the bot's user database * WickrIO API functions used: cmdDecryptString() */ async loadData() { try { if (!fs.existsSync('users.txt')) { console.log('WARNING: users.txt does not exist!') return } const users = fs.readFileSync('users.txt', 'utf-8') if (users.length === 0 || !users || users === '') { return } console.log('Decrypting user database...') const ciphertext = await this.wickrIOAPI.cmdDecryptString(users.toString()) if (encryptorDefined === true) { // Decrypt const decryptedData = encryptor.decrypt(ciphertext) this.wickrUsers = decryptedData } else { this.wickrUsers = JSON.parse(ciphertext) } } catch (err) { console.error(err) } } /* * Decrypts and saves the bot's user database * WickrIO API functions used: cmdEncryptString() */ async saveData() { try { console.log('Encrypting user database...') if (this.wickrUsers.length === 0) { return } let serialusers if (encryptorDefined === true) { // Encrypt serialusers = encryptor.encrypt(this.wickrUsers) } else { serialusers = JSON.stringify(this.wickrUsers) } const encrypted = await this.wickrIOAPI.cmdEncryptString(serialusers) const saved = fs.writeFileSync('users.txt', encrypted, 'utf-8') console.log('User database saved to file!') return true } catch (err) { console.error(err) return false } } /* * Get the transmit queue information. Returns an object that contains * the following formated information: * { * estimated_time: 9999, // estimated time in seconds * count: 99999, // total number of messages left * tx_queue: [ // array of pending transmits * { * message_id: '<messageID>', // Associated message ID * count: 9999, // number of transmits * created: '<date>', // date the broadcast was created * sender: '<sender>', // sender of the message * estimated_time: 9999, // estimated time in seconds * } * ] * } */ async getTransmitQueueInfo() { try { const txQInfo = await this.wickrIOAPI.cmdGetTransmitQueueInfo() console.log('Transmit Queue Info:' + txQInfo) if (txQInfo) { return JSON.parse(txQInfo) } else { return {} } } catch (err) { console.error(err) return {} } } /* * Return the versions of all associated software component */ async getVersions(packageFile) { let reply = '*Versions*' /* * Add the Docker tag */ const dockerInfoFile = '/usr/lib/wickr/docker_info.json' if (fs.existsSync(dockerInfoFile)) { const dockerinfo = JSON.parse(fs.readFileSync(dockerInfoFile, 'utf-8')) const imagetag = dockerinfo.tag if (imagetag) { reply += `\nDocker Tag: ${imagetag}` } } /* * Get the bot client's version information */ let clientVersion = '' const clientInfoJSON = await this.wickrIOAPI.cmdGetClientInfo() if (clientInfoJSON) { const clientInfo = JSON.parse(JSON.stringify(clientInfoJSON)) if (clientInfo.version) { clientVersion = clientInfo.version } } if (clientVersion !== '') reply += `\nBot Client: ${clientVersion}` /* * Add the Integration's version */ try { const packageJson = JSON.parse(fs.readFileSync(packageFile, 'utf-8')) if (packageJson.version) reply += `\nIntegration: ${packageJson.version}` } catch (err) { console.error('getVersions: error getting: ' + packageFile + '\n' + err) } /* * Add the WickrIO Addon's version */ const addonFile = path.join( process.cwd(), 'node_modules/wickrio_addon/package.json' ) try { const addonJson = JSON.parse(fs.readFileSync(addonFile, 'utf-8')) if (addonJson.version) reply += `\nWickrIO Addon: ${addonJson.version}` } catch (err) { console.error('getVersions: error getting: ' + addonFile + '\n' + err) } /* * Add the WickrIO bot API's version */ const botApiFile = path.join( process.cwd(), 'node_modules/wickrio-bot-api/package.json' ) try { const botApiJson = JSON.parse(fs.readFileSync(botApiFile, 'utf-8')) if (botApiJson.version) reply += `\nWickrIO API: ${botApiJson.version}` } catch (err) { console.error('getVersions: error getting: ' + botApiFile + '\n' + err) } return reply } /* * This function parses an incoming message */ parseMessage(message) { let tokens if (process.env.tokens !== undefined) { tokens = JSON.parse(process.env.tokens) } else { tokens = {} } message = JSON.parse(message) const { edit, control, msg_ts, time, receiver, sender, ttl, bor } = message const msgtype = message.msgtype const vGroupID = message.vgroupid let convoType = '' // Get the admin, if this is an admin user const localWickrAdmins = this.myAdmins const admin = localWickrAdmins.getAdmin(sender) // If ONLY admins can receive and handle messages and this is // not an admin, then drop the message if (this.adminOnly === true && admin === undefined) { console.error('Dropping message from non-admin user!') return } // Set the isAdmin flag const isAdmin = admin !== undefined // Determine the convo type (1to1, group, or room) if (vGroupID.charAt(0) === 'S') convoType = 'room' else if (vGroupID.charAt(0) === 'G') convoType = 'groupconvo' else convoType = 'personal' if (message.file) { let isVoiceMemo = false if (message.file.isvoicememo) { isVoiceMemo = true const voiceMemoDuration = message.file.voicememoduration var parsedObj = { file: message.file.localfilename, filename: message.file.filename, vgroupid: vGroupID, control, msgTS: msg_ts, time, receiver, userEmail: sender, isVoiceMemo: isVoiceMemo, voiceMemoDuration: voiceMemoDuration, convotype: convoType, isAdmin: isAdmin, msgtype: 'file', ttl, bor, } } else { var parsedObj = { file: message.file.localfilename, filename: message.file.filename, vgroupid: vGroupID, control, msgTS: msg_ts, time, receiver, userEmail: sender, isVoiceMemo: isVoiceMemo, convotype: convoType, isAdmin: isAdmin, msgtype: 'file', ttl, bor, } } return parsedObj } else if (message.location) { var parsedObj = { latitude: message.location.latitude, longitude: message.location.longitude, vgroupid: vGroupID, control, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'location', ttl, bor, } return parsedObj } else if (message.call) { var parsedObj = { status: message.call.status, vgroupid: vGroupID, call: message.call, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'call', ttl, bor, } return parsedObj } else if (message.keyverify) { var parsedObj = { vgroupid: vGroupID, control, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'keyverify', ttl, bor, } return parsedObj } else if (message.control) { var parsedObj = { vgroupid: vGroupID, control, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'control', ttl, bor, } return parsedObj } else if (message.edit) { var parsedObj = { vgroupid: vGroupID, edit, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'edit', ttl, bor, } return parsedObj } else if (message.msgtype === 4003 || message.msgtype === 4005) { // Control leave or remove message const controlNew={ msgtype: message.msgtype } var parsedObj = { vgroupid: vGroupID, control: controlNew, msgTS: msg_ts, time, receiver, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'control', ttl, bor, } return parsedObj } else if (message.message === undefined) { return } const request = message.message let command = '' let argument = '' // This doesn't capture @ mentions const parsedData = request.trim().match(/^(\/[a-zA-Z]+)([\s\S]*)$/) if (parsedData !== null) { command = parsedData[1] if (parsedData[2] !== '') { argument = parsedData[2] .trim() .replace(/^@[^ ]+ /, '') .trim() } } // If this is an admin then process any admin commands if ( tokens.ADMINISTRATORS_CHOICE && tokens.ADMINISTRATORS_CHOICE.value === 'yes' && admin !== undefined ) { localWickrAdmins.processAdminCommand(sender, vGroupID, command, argument) } var parsedObj = { message: request, command: command, msgTS: msg_ts, time, receiver, argument: argument, vgroupid: vGroupID, userEmail: sender, convotype: convoType, isAdmin: isAdmin, msgtype: 'message', ttl, bor, } return parsedObj } getMessage({ rawMessage }) { console.log({ rawMessage }) // const tokens = JSON.parse(process.env.tokens) const jsonmsg = JSON.parse(rawMessage) const { message_id: messageID, message, edit, control, file, msg_ts: msgTS, time, receiver, sender: userEmail, ttl, location, vgroupid: vGroupID, msgtype: msgType, call, users, keyverify, } = jsonmsg let { bor } = jsonmsg if (!bor) bor = 0 // const msgtype = message.msgtype // const vGroupID = message.vgroupid let convoType = '' // Get the admin, if this is an admin user const localWickrAdmins = this.myAdmins const admin = localWickrAdmins.getAdmin(userEmail) // If ONLY admins can receive and handle messages and this is // not an admin, then drop the message if (this.adminOnly === true && admin === undefined) { console.log('Dropping message from non-admin user!') return } // Set the isAdmin flag const isAdmin = admin !== undefined // Determine the convo type (1to1, group, or room) if (vGroupID.charAt(0) === 'S') convoType = 'room' else if (vGroupID.charAt(0) === 'G') convoType = 'groupconvo' else convoType = 'personal' let parsedMessage = { messageID, message, msgTS, time, receiver, users, vGroupID, userEmail, convoType, isAdmin, ttl, bor, } if (file) { if (file.isvoicememo) { parsedMessage = { ...parsedMessage, file: file.localfilename, filename: file.filename, isVoiceMemo: true, voiceMemoDuration: file.voicememoduration, msgType: 'file', } return parsedMessage } else { parsedMessage = { ...parsedMessage, file: file.localfilename, filename: file.filename, isVoiceMemo: false, msgType: 'file', } } return parsedMessage } else if (location) { parsedMessage = { ...parsedMessage, latitude: location.latitude, longitude: location.longitude, msgType: 'location', } return parsedMessage } else if (call) { parsedMessage = { ...parsedMessage, status: call.status, call, msgType: 'call', } return parsedMessage } else if (keyverify) { parsedMessage = { ...parsedMessage, control, msgType: 'keyverify', } return parsedMessage } else if (control) { if (control.isrecall) { parsedMessage = { ...parsedMessage, msgType: 'delete', } } else { parsedMessage = { ...parsedMessage, control, msgType: 'edit', } } return parsedMessage } else if (edit) { parsedMessage = { ...parsedMessage, msgType: 'edit', } return parsedMessage } else if (message === undefined) { return } let command = '' let argument = '' // This doesn't capture @ mentions const parsedData = message.trim().match(/^(\/[a-zA-Z]+)([\s\S]*)$/) if (parsedData !== null) { command = parsedData[1] if (parsedData[2] !== '') { argument = parsedData[2] .trim() .replace(/^@[^ ]+ /, '') .trim() } } // If this is an admin then process any admin commands if (admin !== undefined) { localWickrAdmins.processAdminCommand( userEmail, vGroupID, command, argument ) } parsedMessage = { ...parsedMessage, command, argument, } return parsedMessage } /* * User functions */ addUser(wickrUser) { this.wickrUsers.push(wickrUser) const saved = this.saveData() console.log('New Wickr user added to database.') return wickrUser } getUser(userEmail) { const found = this.wickrUsers.find(function (user) { return user.userEmail === userEmail }) return found } getUsers() { return this.wickrUsers } deleteUser(userEmail) { const found = this.wickrUsers.find(function (user) { return user.userEmail === userEmail }) const index = this.wickrUsers.indexOf(found) this.wickrUsers.splice(index, 1) return found } /* * Admin functions */ getAdmins() { const localWickrAdmins = this.myAdmins return localWickrAdmins.getAdmins() } /* * copy processes.json tokens to process.env */ processesJsonToProcessEnv() { console.log('Copying processes.json tokens to process.env') // Read in the processes.json file const processesJsonFile = path.join(process.cwd(), 'processes.json') if (!fs.existsSync(processesJsonFile)) { console.error(processesJsonFile + ' does not exist!') return false } const processesJson = fs.readFileSync(processesJsonFile) // console.log('processes.json=' + processesJson) const processesJsonObject = JSON.parse(processesJson) process.env.tokens = JSON.stringify(processesJsonObject.apps[0].env.tokens) // console.log('end process.env=' + JSON.stringify(process.env)) // console.log('end process.env.tokens=' + process.env.tokens) return true } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } module.exports = { WickrIOBot, WickrUser, WickrIOConfigure, logger, }