import path from 'path'; import net from 'net'; import util from 'util'; import cprog from 'child_process'; import convert from 'convert-units'; import { Plugin, Configurable, EventListener, DependencyLoad } from '@squeebot/core/lib/plugin'; import { IMessage } from '@squeebot/core/lib/types'; import { httpGET, parseTimeToSeconds, readableTime } from '@squeebot/core/lib/common'; import { logger } from '@squeebot/core/lib/core'; type CEXResponse = {[key: string]: number}; const cexCache: {[key: string]: number | CEXResponse} = { expiry: 0, date: 0, cache: {}, }; const bases: {[key: number]: string[]} = { 2: ['bin', 'binary'], 8: ['oct', 'octal'], 10: ['dec', 'decimal'], 16: ['hex', 'hexadecimal'] }; function getBaseNum(base: string): number | null { let result = null; for (const i in bases) { const defs = bases[i]; if (defs.indexOf(base) === -1) { continue; } result = parseInt(i, 10); } if (result) { return result; } if (base.indexOf('b') !== 0) { return null; } const matcher = base.match(/b(?:ase-?)?(\d+)/); if (!matcher || !matcher[1] || isNaN(parseInt(matcher[1], 10))) { return null; } return parseInt(matcher[1], 10); } const urlRegex = /(((ftp|https?):\/\/)[-\w@:%_+.~#?,&//=]+)/g; const fork = cprog.fork; // Run mathjs in a separate thread to avoid the killing of the main process function opMath(expression: string, method = 'eval'): Promise { return new Promise((resolve, reject) => { // Fork the script const mathThread = fork(path.join(__dirname, 'math.js')); let done = false; // Time the request out when user enters something too complex const timeItOut = setTimeout(() => { mathThread.kill('SIGKILL'); done = true; return reject(new Error('Timed out')); }, 8000); // Send data to the thread to process mathThread.send(method + ' ' + expression); // Recieve data mathThread.on('message', (chunk) => { clearTimeout(timeItOut); if (done) { return; } const line = chunk.toString().trim(); if (line.length > 280) { return reject(new Error('The response was too large')); } done = true; if (line === 'null') { return reject(new Error('Nothing was returned')); } resolve(line); }); mathThread.on('exit', () => { clearTimeout(timeItOut); if (!done) { reject(new Error('Nothing was returned')); } }); }); } function rgbToHex(r: number, g: number, b: number): string { // tslint:disable-next-line: no-bitwise return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } function hexToRgb(hex: string): {[key: string]: number} | null { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, (m, r, g, b) => { return r + r + g + g + b + b; }); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } function pingTcpServer(host: string, port: number): Promise { return new Promise((resolve, reject) => { let isFinished = false; let timeA = new Date().getTime(); const timeB = new Date().getTime(); function returnResults(status: boolean, info: number | Error): void { if (!isFinished) { isFinished = true; if (info instanceof Error) { return reject(info); } resolve(info); } } const pingHost = net.connect({port, host}, () => { timeA = new Date().getTime(); returnResults(true, timeA - timeB); pingHost.end(); pingHost.destroy(); }); pingHost.setTimeout(5000); pingHost.on('timeout', () => { pingHost.end(); pingHost.destroy(); returnResults(false, new Error('timeout')); }); pingHost.on('error', (e) => { pingHost.end(); pingHost.destroy(); returnResults(false, e); }); pingHost.on('close', () => { returnResults(false, new Error('closed')); }); }); } function addCommands(plugin: UtilityPlugin, commands: any): void { const cmds = []; cmds.push({ name: 'binary', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { let response = ''; let strArr; let i; let text = msg.data.split(' ').slice(2).join(' '); try { switch (simplified[0] ? simplified[0].toUpperCase() : null) { case 'ENCODE': strArr = text.split(''); for (i in strArr) { response += ' ' + ('0000000' + parseInt(Buffer.from(strArr[i].toString(), 'utf8').toString('hex'), 16).toString(2)).slice(-8); } response = response.substr(1); break; case 'DECODE': text = text.split(' ').join(''); i = 0; while (8 * (i + 1) <= text.length) { response += Buffer.from(parseInt(text.substr(8 * i, 8), 2).toString(16), 'hex').toString('utf8'); i++; } response = 'Decoded: ' + response.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); } } catch (e) { msg.resolve('Operation failed.'); return true; } msg.resolve(response); return true; }, description: 'Encode/decode binary (ASCII only)', usage: ' ' }); cmds.push({ name: 'hexstr', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { let response = ''; let i; let text = msg.data.split(' ').slice(2).join(' '); try { switch (simplified[0] ? simplified[0].toUpperCase() : null) { case 'DECODE': text = text.replace(/\s/g, ''); for (i = 0; i < text.length; i += 2) { response += String.fromCharCode(parseInt(text.substr(i, 2), 16)); } response = 'Decoded: ' + response.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); break; case 'ENCODE': for (i = 0; i < text.length; i++) { response += text.charCodeAt(i).toString(16) + ' '; } break; } } catch (e) { msg.resolve('Operation failed.'); return true; } msg.resolve(response); return true; }, description: 'Encode/decode hexadecimal (ASCII only)', usage: ' ' }); cmds.push({ name: 'base64', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { let response = ''; const text = msg.data.split(' ').slice(2).join(' '); try { switch (simplified[0] ? simplified[0].toUpperCase() : null) { case 'DECODE': response = 'Decoded: ' + (Buffer.from(text, 'base64').toString('ascii')).replace(/\n/g, '\\n').replace(/\r/g, '\\r'); break; case 'ENCODE': response = Buffer.from(text).toString('base64'); break; } } catch (e) { msg.resolve('Operation failed.'); return true; } msg.resolve(response); return true; }, description: 'Encode/decode base64 (ASCII only)', usage: ' ' }); cmds.push({ name: 'numsys', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { const input = simplified[0]; const src = simplified[1].toLowerCase(); const dst = simplified[2].toLowerCase(); const srcBase = getBaseNum(src); const dstBase = getBaseNum(dst); if (!srcBase || !dstBase || dstBase > 36 || dstBase < 2 || srcBase > 36 || srcBase < 2) { msg.resolve('Invalid conversion.'); return false; } const decimal = parseInt(input, srcBase); const result = decimal.toString(dstBase); msg.resolve('Result:', result); return true; }, description: 'Convert a value into a value in another numbering system.', usage: ' ', aliases: ['convertnumbers', 'cvnums'] }); cmds.push({ name: 'convertseconds', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { msg.resolve(readableTime(parseInt(simplified[0], 10))); return true; }, description: 'Convert seconds to years days hours minutes seconds.', usage: '', aliases: ['cvs', 'parseseconds'] }); cmds.push({ name: 'converttime', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { const str = msg.data.split(' ').slice(1).join(' '); if (!str) { msg.resolve('Invalid input'); return true; } msg.resolve(parseTimeToSeconds(str) + ' seconds'); return true; }, description: 'Convert ywdhms to seconds.', usage: '[y] [w] [d] [h] [m] [s]', aliases: ['cvt', 'parsetime'] }); cmds.push({ name: 'reconverttime', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { const str = msg.data.split(' ').slice(1).join(' '); if (!str) { msg.resolve('Invalid input'); return true; } const sec = parseTimeToSeconds(str); msg.resolve(readableTime(sec)); return true; }, aliases: ['rcvt'] }); cmds.push({ name: 'eval', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { return true; } const wholeRow = msg.data.split(' ').slice(1).join(' '); opMath(wholeRow).then((repl) => { msg.resolve(repl); }, (e) => { msg.resolve('Could not evaluate expression:', e.message); }); return true; }, aliases: ['evaluate', 'math', 'equation', 'calc'], usage: '', description: 'Evaluate a math expression' }); cmds.push({ name: 'simplify', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { return true; } const wholeRow = msg.data.split(' ').slice(1).join(' '); opMath(wholeRow, 'simplify').then((repl) => { msg.resolve(repl); }, () => { msg.resolve('Could not evaluate expression!'); }); return true; }, aliases: ['mathsimple', 'algebra'], usage: '', description: 'Simplify a math expression' }); cmds.push({ name: 'evaljs', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { return true; } const script = msg.data.split(' ').slice(1).join(' '); // Disallow child_process when shell is disallowed if ((script.indexOf('child_process') !== -1 || script.indexOf('cprog') !== -1 || script.indexOf('fork') !== -1) && !plugin.config.config.allowShell) { msg.resolve('Error: child_process is not allowed in evaljs due to security reasons.'); return true; } try { const mesh = eval(script); /* eslint no-eval: off */ if (mesh === undefined) { return true; } msg.resolve(util.format(mesh)); } catch (e) { msg.resolve('Error: ' + e.message); } return true; }, description: 'Execute JavaScript in a command context', permissions: ['system_execute'], hidden: true }); cmds.push({ name: 'userid', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { msg.resolve('Your userId is %s.', msg.fullSenderID); return true; }, description: 'Display your userId (internal user identification)', hidden: true }); cmds.push({ name: 'roomid', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { msg.resolve('Current roomId is %s.', msg.fullRoomID); return true; }, description: 'Display the internal identification of this room', hidden: true }); cmds.push({ name: 'serverid', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (msg.target && msg.target.server) { msg.resolve('Current server ID is s:%s.', msg.target.server); return true; } msg.resolve('This protocol does not specify a server. ' + 'Either the protocol is for a single server only or the server variable is not supported.'); return true; }, description: 'Display the internal identification of this room', hidden: true }); cmds.push({ name: 'rgb2hex', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { return true; } const fullmsg = msg.data.split(' ').slice(1).join(' '); const channels = fullmsg.match(/(rgb)?\(?(\d{1,3}),?\s(\d{1,3}),?\s(\d{1,3})\)?/i); if (!channels || channels[2] == null) { msg.resolve('Invalid parameter'); return true; } const r = parseInt(channels[2], 10); const g = parseInt(channels[3], 10); const b = parseInt(channels[4], 10); if (r > 255 || g > 255 || b > 255) { msg.resolve('Invalid colors'); return true; } const hex = rgbToHex(r, g, b); msg.resolve(hex); return true; }, description: 'Convert RGB to HEX colors', usage: '[rgb](, , )| ' }); cmds.push({ name: 'hex2rgb', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { return true; } let hexcode = simplified[0]; if (hexcode.indexOf('#') === -1) { hexcode = '#' + hexcode; } if (hexcode.length !== 4 && hexcode.length !== 7) { msg.resolve('Invalid length'); return true; } const rgb = hexToRgb(hexcode); if (!rgb) { msg.resolve('Invalid HEX notation'); return true; } msg.resolve('rgb(%d, %d, %d)', rgb.r, rgb.g, rgb.b); return true; }, description: 'Convert HEX to RGB colors', usage: '#|#' }); cmds.push({ name: 'isup', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { if (!simplified[0]) { msg.resolve('Please specify host name!'); return true; } if (!simplified[1]) { msg.resolve('Please specify port!'); return true; } const host = simplified[0]; const port = parseInt(simplified[1], 10); if (isNaN(port) || port <= 0 || port > 65535) { msg.resolve('Invalid port number!'); return true; } let statusString = msg.source.format.format('bold', 'closed'); let status; try { status = await pingTcpServer(host, port); statusString = msg.source.format.format('bold', 'open'); } catch (e) { status = e.message; } if (!isNaN(parseFloat(status))) { status = status + ' ms'; } msg.resolve(`Port ${port} on ${host} is ${statusString} (${status})`); return true; }, description: 'Ping a host', usage: ' ', aliases: ['tcpup', 'tping'], hidden: true }); cmds.push({ name: 'convert', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { const tqnt = parseFloat(simplified[0]); if (isNaN(tqnt)) { msg.resolve('Please specify a quantity, either an integer or a float!'); return true; } const src = simplified[1]; let dst = simplified[2]; if (dst && dst.toLowerCase() === 'to') { dst = simplified[3]; } if (!src) { msg.resolve('Please specify source unit!'); return true; } if (!dst) { msg.resolve('Please specify destination unit!'); return true; } let res = null; try { res = convert(tqnt).from(src).to(dst); } catch (e) { res = null; } if (res) { const srcdesc = convert().describe(src); const dstdesc = convert().describe(dst); const bsrcdesc = (Math.floor(tqnt) !== 1) ? srcdesc.plural : srcdesc.singular; const bdstdesc = (Math.floor(res) !== 1) ? dstdesc.plural : dstdesc.singular; msg.resolve(` ${tqnt} ${bsrcdesc} => ${res} ${bdstdesc}`); return true; } msg.resolve('Failed to convert.'); return true; }, description: 'Convert between quantities in different units.', usage: ' ', aliases: ['cv', 'unit'] }); cmds.push({ name: 'currency', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { let ctw: CEXResponse | null = cexCache.cache as CEXResponse; if (cexCache.expiry < Date.now()) { let fetched; try { const data = await httpGET('https://api.exchangeratesapi.io/latest'); fetched = JSON.parse(data); logger.log('[utility] Fetched currency exchange rates successfully.'); } catch (e) { ctw = null; } if (!ctw || !fetched.rates) { msg.resolve('Could not fetch currency exchange rates at this time. Please try again later.'); return true; } cexCache.cache = fetched.rates; cexCache.date = fetched.date; cexCache.expiry = Date.now() + 86400000; // day ctw = cexCache.cache as CEXResponse; } if (simplified[0] === 'date') { msg.resolve('Currency exchange rates are as of %s', cexCache.date); return true; } else if (simplified[0] === 'list') { msg.resolve('Currently supported currencies: EUR, %s', Object.keys(cexCache.cache).join(', ')); return true; } const n = parseFloat(simplified[0]); let f = simplified[1]; let t = simplified[2]; if (isNaN(n) || !f || !t) { msg.resolve('Invalid parameters.'); return true; } f = f.toUpperCase(); t = t.toUpperCase(); if (f !== 'EUR' && !ctw[f]) { msg.resolve('This currency is currently not supported.'); return true; } if (t !== 'EUR' && !ctw[t]) { msg.resolve('This currency is currently not supported.'); return true; } if (f === t) { msg.resolve('%f %s', n, f); return true; } let ramnt: string; if (f === 'EUR') { ramnt = (n * ctw[t]).toFixed(4); msg.resolve('%f EUR => %f %s', n, ramnt, t); return true; } else if (t === 'EUR') { ramnt = (n / ctw[f]).toFixed(4); msg.resolve('%f %s => %f EUR', n, f, ramnt); return true; } const amnt = (ctw[t] * n / ctw[f]).toFixed(4); msg.resolve('%f %s => %f %s', n, f, amnt, t); return true; }, description: 'Convert between currencies.', usage: ' | [date | list] [] []', aliases: ['cex', 'exchange'] }); cmds.push({ name: 'randomnumber', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { let min = parseInt(simplified[0], 10); let max = parseInt(simplified[1], 10); let count = parseInt(simplified[2], 10); const countMax = plugin.config.config.randomMax || 64; if (isNaN(min) || isNaN(max)) { msg.resolve('Invalid numbers.'); return true; } if (min > max) { const realMax = min + 0; min = max; max = realMax; } if (isNaN(count)) { count = 1; } if (String(Math.abs(min)).length > 9 || String(Math.abs(max)).length > 9) { msg.resolve('The numbers are too large!'); return true; } if (count > countMax) { msg.resolve('Too many to generate. Maximum: ' + countMax); return true; } const numbers = []; for (let i = 0; i < count; i++) { numbers.push(Math.floor(Math.random() * (max - min + 1)) + min); } msg.resolve(numbers.join(' ')); return true; }, description: 'Generate a random number between and .', usage: ' []', aliases: ['rnum', 'rand'] }); if (plugin.config.config.allowShell) { logger.warn('WARNING! Shell command execution is enabled! Make absolutely sure that there is proper authentication!'); if (process.getuid && process.getuid() === 0) { logger.warn('NEVER run Squeebot as root! Run `useradd squeebot`! We are not responsible for possible security leaks!'); } cmds.push({ name: 'sh', execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise => { const stripnl = (simplified[0] !== '-n'); const cmd = simplified.slice(stripnl ? 0 : 1).join(' '); if (!cmd) { msg.resolve('Nothing to execute!'); return true; } cprog.exec(cmd, {shell: '/bin/bash'}, (error, stdout, stderr) => { if (stdout) { if (stripnl) { stdout = stdout.replace(/\n/g, ' ;; '); } return msg.resolve(stdout); } msg.resolve('Error executing command.'); logger.error(stderr || error); }); return true; }, description: 'Run raw shell command.', usage: '', hidden: true, permissions: ['system_execute'], }); } commands.registerCommand(cmds.map((x: any) => { x.plugin = plugin.manifest.name; return x; })); } @Configurable({ allowShell: false, googleapikey: null, ipfsGateway: 'https://ipfs.io', randomMax: 64 }) class UtilityPlugin extends Plugin { bindEvents(): void { this.on('message', (msg: IMessage) => { // Pre-regex check if (msg.data.indexOf('ipfs://') === -1 && msg.data.indexOf('Qm') === -1) { return; } // IPFS urlify const mmatch = msg.data.match(/(?:ipfs:\/\/|\s|^)(Qm[\w\d]{44})(?:\s|$)/); if (mmatch && mmatch[1]) { msg.resolve(this.config.config.ipfsGateway + '/ipfs/' + mmatch[1]); } }); } @DependencyLoad('simplecommands') addCommands(cmd: any): void { addCommands(this, cmd); } @EventListener('pluginUnload') public unloadEventHandler(plugin: string | Plugin): void { if (plugin === this.name || plugin === this) { this.config.save().then(() => this.emit('pluginUnloaded', this)); } } initialize(): void { this.bindEvents(); this.emit('pluginLoaded', this); } } module.exports = UtilityPlugin;