plugins-evert/utility/plugin.ts

679 lines
19 KiB
TypeScript
Raw Normal View History

2020-12-03 19:27:14 +00:00
import path from 'path';
import net from 'net';
import { fork } from 'child_process';
2020-12-03 19:27:14 +00:00
import {
Plugin,
Configurable,
EventListener,
DependencyLoad
} from '@squeebot/core/lib/plugin';
2020-12-13 10:16:49 +00:00
import { IMessage, MessageResolver } from '@squeebot/core/lib/types';
2020-12-03 19:27:14 +00:00
import { httpGET, parseTimeToSeconds, readableTime } from '@squeebot/core/lib/common';
import { logger } from '@squeebot/core/lib/core';
import {
createUnitIndex,
getUnitAbbreviation,
hexToRgb,
rgbToHex,
RGBToHSL,
convert,
wipeCaches,
ASCIIBinaryConverter,
ASCIIHexConverter,
base64Converter,
baseConverter
} from './convert';
type CEXResponse = { [key: string]: number };
const cexCache: { [key: string]: number | CEXResponse } = {
2020-12-03 19:27:14 +00:00
expiry: 0,
date: 0,
cache: {},
};
// Run mathjs in a separate thread to avoid the killing of the main process
2020-12-06 13:41:18 +00:00
function opMath(expression: string): Promise<string> {
2020-12-03 19:27:14 +00:00
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
2020-12-06 13:41:18 +00:00
mathThread.send(expression);
2020-12-03 19:27:14 +00:00
// 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 pingTcpServer(host: string, port: number): Promise<number> {
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',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
try {
const result = ASCIIBinaryConverter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
2020-12-03 19:27:14 +00:00
}
return true;
},
description: 'Encode/decode binary (ASCII only)',
usage: '<ENCODE/DECODE> <message>'
});
cmds.push({
name: 'hexstr',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
try {
const result = ASCIIHexConverter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
2020-12-03 19:27:14 +00:00
}
return true;
},
description: 'Encode/decode hexadecimal (ASCII only)',
usage: '<ENCODE/DECODE> <string>'
});
cmds.push({
name: 'base64',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
try {
const result = base64Converter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
2020-12-03 19:27:14 +00:00
}
return true;
},
description: 'Encode/decode base64 (ASCII only)',
usage: '<ENCODE/DECODE> <string>'
});
cmds.push({
name: 'numsys',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
try {
const result = baseConverter(simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
2020-12-03 19:27:14 +00:00
}
return true;
},
description: 'Convert a value into a value in another numbering system.',
usage: '<value> <bin/dec/hex/oct> <bin/dec/hex/oct>',
aliases: ['convertnumbers', 'cvnums']
});
cmds.push({
name: 'convertseconds',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
msg.resolve(readableTime(parseInt(simplified[0], 10)));
return true;
},
description: 'Convert seconds to years days hours minutes seconds.',
usage: '<seconds>',
aliases: ['cvs', 'parseseconds']
});
cmds.push({
name: 'converttime',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-05 10:00:00 +00:00
const str = msg.text.split(' ').slice(1).join(' ');
2020-12-03 19:27:14 +00:00
if (!str) {
msg.resolve('Invalid input');
return true;
}
msg.resolve(parseTimeToSeconds(str), 'seconds');
2020-12-03 19:27:14 +00:00
return true;
},
description: 'Convert ywdhms to seconds.',
usage: '[<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]',
aliases: ['cvt', 'parsetime']
});
cmds.push({
name: 'reconverttime',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-05 10:00:00 +00:00
const str = msg.text.split(' ').slice(1).join(' ');
2020-12-03 19:27:14 +00:00
if (!str) {
msg.resolve('Invalid input');
return true;
}
const sec = parseTimeToSeconds(str);
msg.resolve(readableTime(sec));
return true;
},
aliases: ['rcvt']
});
cmds.push({
name: 'eval',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
if (!simplified[0]) {
return true;
}
2020-12-05 10:00:00 +00:00
const wholeRow = msg.text.split(' ').slice(1).join(' ');
2020-12-03 19:27:14 +00:00
2020-12-05 10:00:00 +00:00
try {
const repl = await opMath(wholeRow);
2020-12-03 19:27:14 +00:00
msg.resolve(repl);
} catch (e: any) {
2020-12-03 19:27:14 +00:00
msg.resolve('Could not evaluate expression:', e.message);
2020-12-05 10:00:00 +00:00
}
2020-12-03 19:27:14 +00:00
return true;
},
2020-12-06 13:41:18 +00:00
aliases: ['math', 'calc'],
2020-12-03 19:27:14 +00:00
usage: '<expression>',
2020-12-06 13:41:18 +00:00
description: 'Evaluate a math expression (See https://mathjs.org/)'
2020-12-03 19:27:14 +00:00
});
cmds.push({
name: 'userid',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
msg.resolve('Your userId is %s.', msg.fullSenderID);
return true;
},
description: 'Display your userId (internal user identification)',
hidden: true
});
cmds.push({
name: 'roomid',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
msg.resolve('Current roomId is %s.', msg.fullRoomID);
return true;
},
description: 'Display the internal identification of this room',
hidden: true
});
cmds.push({
name: 'serverid',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
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',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
if (!simplified[0]) {
return true;
}
2020-12-05 10:00:00 +00:00
const fullmsg = msg.text.split(' ').slice(1).join(' ');
2020-12-03 19:27:14 +00:00
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;
}
2020-12-06 13:41:18 +00:00
msg.resolve(rgbToHex(r, g, b));
2020-12-03 19:27:14 +00:00
return true;
},
description: 'Convert RGB to HEX colors',
usage: '[rgb](<r>, <g>, <b>)|<r> <g> <b>'
});
2020-12-05 10:00:00 +00:00
cmds.push({
name: 'rgb2hsl',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-05 10:00:00 +00:00
if (!simplified[0]) {
return true;
}
const fullmsg = msg.text.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 hsl = RGBToHSL(r, g, b);
msg.resolve('hsl(%d, %d%, %d%)', hsl.h, hsl.s, hsl.l);
2020-12-05 10:00:00 +00:00
return true;
},
description: 'Convert RGB to HSL colors',
usage: '[rgb](<r>, <g>, <b>)|<r> <g> <b>'
});
2020-12-03 19:27:14 +00:00
cmds.push({
name: 'hex2rgb',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
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: '#<r><g><b>|#<rr><gg><bb>'
});
cmds.push({
name: 'isup',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
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: any) {
2020-12-03 19:27:14 +00:00
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: '<host> <port>',
aliases: ['tcpup', 'tping'],
hidden: true
});
cmds.push({
name: 'convert',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-03 19:27:14 +00:00
const tqnt = parseFloat(simplified[0]);
if (isNaN(tqnt)) {
msg.resolve('Please specify a quantity, either an integer or a float!');
return true;
}
let src = simplified[1];
2020-12-03 19:27:14 +00:00
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;
src = getUnitAbbreviation(src);
dst = getUnitAbbreviation(dst);
if (!src) {
msg.resolve('Source unit not found!');
return true;
}
if (!dst) {
msg.resolve('Destination unit not found!');
return true;
}
2020-12-03 19:27:14 +00:00
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.',
2020-12-03 19:27:14 +00:00
usage: '<number> <from unit> <to unit>',
aliases: ['cv', 'unit']
});
2021-04-15 16:56:45 +00:00
cmds.push({
name: 'currency',
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
let ctw: CEXResponse | null = cexCache.cache as CEXResponse;
if (cexCache.expiry < Date.now()) {
let fetched;
try {
const data = await httpGET('https://api.exchangerate.host/latest');
fetched = JSON.parse(data);
logger.log('[utility] Fetched currency exchange rates successfully.');
} catch (e) {
ctw = null;
2020-12-03 19:27:14 +00:00
}
2021-04-15 16:56:45 +00:00
if (!ctw || !fetched.rates) {
msg.resolve('Could not fetch currency exchange rates at this time. Please try again later.');
2020-12-03 19:27:14 +00:00
return true;
}
2021-04-15 16:56:45 +00:00
cexCache.cache = fetched.rates;
cexCache.date = fetched.date;
cexCache.expiry = Date.now() + 86400000; // day
ctw = cexCache.cache as CEXResponse;
}
2020-12-03 19:27:14 +00:00
2021-04-15 16:56:45 +00:00
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;
}
2020-12-03 19:27:14 +00:00
2021-04-15 16:56:45 +00:00
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();
2020-12-03 19:27:14 +00:00
2021-04-15 16:56:45 +00:00
if (f !== 'EUR' && !ctw[f]) {
msg.resolve('This currency is currently not supported.');
return true;
}
2020-12-03 19:27:14 +00:00
2021-04-15 16:56:45 +00:00
if (t !== 'EUR' && !ctw[t]) {
msg.resolve('This currency is currently not supported.');
return true;
}
2020-12-03 19:27:14 +00:00
2021-04-15 16:56:45 +00:00
if (f === t) {
msg.resolve('%f %s', n, f);
2020-12-03 19:27:14 +00:00
return true;
2021-04-15 16:56:45 +00:00
}
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: '<number> | [date | list] [<from currency>] [<to currency>]',
aliases: ['cex', 'exchange']
});
2020-12-03 19:27:14 +00:00
cmds.push({
name: 'randomnumber',
2020-12-13 10:16:49 +00:00
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
2020-12-06 13:41:18 +00:00
if (simplified.length < 2) {
msg.resolve('Too few arguments!');
return true;
}
2020-12-03 19:27:14 +00:00
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 <min> and <max>.',
usage: '<min> <max> [<count>]',
2020-12-06 13:41:18 +00:00
aliases: ['rnum', 'rand', 'rng']
2020-12-03 19:27:14 +00:00
});
// FIXME: temporary code for removal
cmds.push({
name: 'cmtest',
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
console.log(simplified);
msg.resolve(`argument 1: '${simplified[0]}' argument 2: '${simplified[1]}'`);
return true;
},
description: 'Test the command argument parser',
usage: '',
hidden: true,
});
2020-12-03 19:27:14 +00:00
commands.registerCommand(cmds.map((x: any) => {
x.plugin = plugin.manifest.name;
return x;
}));
}
@Configurable({
ipfsGateway: 'https://ipfs.io',
randomMax: 64
})
class UtilityPlugin extends Plugin {
@DependencyLoad('simplecommands')
addCommands(cmd: any): void {
addCommands(this, cmd);
}
@EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
wipeCaches();
2021-10-02 09:31:46 +00:00
this.emit('pluginUnloaded', this);
2020-12-03 19:27:14 +00:00
}
}
initialize(): void {
2020-12-05 10:00:00 +00:00
this.on('message', (msg: IMessage) => {
// Pre-regex check
if (msg.text.indexOf('ipfs://') === -1 && msg.text.indexOf('Qm') === -1) {
return;
}
// IPFS urlify
const mmatch = msg.text.match(/(?:ipfs:\/\/|\s|^)(Qm[\w\d]{44})(?:\s|$)/);
if (mmatch && mmatch[1]) {
msg.resolve(this.config.config.ipfsGateway + '/ipfs/' + mmatch[1]);
}
});
// Initialize list of units
createUnitIndex();
2020-12-03 19:27:14 +00:00
}
}
module.exports = UtilityPlugin;