plugins-evert/utility/plugin.ts

823 lines
20 KiB
TypeScript

import path from 'path';
import net from 'net';
import { fork } from 'child_process';
import {
Plugin,
Configurable,
EventListener,
DependencyLoad,
} from '@squeebot/core/lib/plugin';
import { IMessage, MessageResolver } from '@squeebot/core/lib/types';
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 };
// Run mathjs in a separate thread to avoid the killing of the main process
function opMath(expression: string): Promise<string> {
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(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 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',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
try {
const result = ASCIIBinaryConverter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
}
return true;
},
description: 'Encode/decode binary (ASCII only)',
usage: '<ENCODE/DECODE> <message>',
});
cmds.push({
name: 'hexstr',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
try {
const result = ASCIIHexConverter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
}
return true;
},
description: 'Encode/decode hexadecimal (ASCII only)',
usage: '<ENCODE/DECODE> <string>',
});
cmds.push({
name: 'base64',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
try {
const result = base64Converter(msg.text, simplified);
msg.resolve(`> ${result}`);
} catch (e: any) {
msg.resolve('Failed to convert.');
}
return true;
},
description: 'Encode/decode base64 (ASCII only)',
usage: '<ENCODE/DECODE> <string>',
});
cmds.push({
name: 'numsys',
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.');
}
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',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
const str = msg.text.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:
'[<years>y] [<weeks>w] [<days>d] [<hours>h] [<minutes>m] [<seconds>s]',
aliases: ['cvt', 'parsetime'],
});
cmds.push({
name: 'reconverttime',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
const str = msg.text.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,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
if (!simplified[0]) {
return true;
}
const wholeRow = msg.text.split(' ').slice(1).join(' ');
try {
const repl = await opMath(wholeRow);
msg.resolve(repl);
} catch (e: any) {
msg.resolve('Could not evaluate expression:', e.message);
}
return true;
},
aliases: ['math', 'calc'],
usage: '<expression>',
description: 'Evaluate a math expression (See https://mathjs.org/)',
});
cmds.push({
name: 'userid',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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;
}
msg.resolve(rgbToHex(r, g, b));
return true;
},
description: 'Convert RGB to HEX colors',
usage: '[rgb](<r>, <g>, <b>)|<r> <g> <b>',
});
cmds.push({
name: 'rgb2hsl',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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);
return true;
},
description: 'Convert RGB to HSL colors',
usage: '[rgb](<r>, <g>, <b>)|<r> <g> <b>',
});
cmds.push({
name: 'hex2rgb',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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) {
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',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
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];
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;
}
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: '<number> <from unit> <to unit>',
aliases: ['cv', 'unit'],
});
cmds.push({
name: 'currency',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
let cexData: CEXResponse | null = plugin.cexCache.cache as CEXResponse;
const currentStamp = new Date().toISOString().replace(/T(.*)$/, '');
if (plugin.cexCache.expiry < Date.now()) {
let fetched;
try {
const data = await httpGET(`https://api.exchangerate.host/latest?v=${currentStamp}`);
fetched = JSON.parse(data);
logger.log('[utility] Fetched currency exchange rates successfully.');
} catch (e) {
fetched = null;
}
if (!fetched?.rates) {
msg.resolve(
'Could not fetch currency exchange rates at this time. Please try again later.'
);
return true;
}
Object.assign(plugin.cexCache, {
cache: fetched.rates,
date: fetched.date,
expiry: Date.now() + 86400000 / 2 // half-day
});
cexData = fetched.rates as CEXResponse;
}
if (simplified[0] === 'date') {
msg.resolve(
'Currency exchange rates are as of %s',
plugin.cexCache.date
);
return true;
} else if (simplified[0] === 'list') {
msg.resolve(
'Currently supported currencies: %s',
Object.keys(cexData).join(', ')
);
return true;
}
const inputValue = parseFloat(simplified[0]);
let fromCurrency = simplified[1];
let toCurrency = simplified[2];
if (isNaN(inputValue) || !fromCurrency || !toCurrency) {
msg.resolve('Invalid parameters.');
return true;
}
fromCurrency = fromCurrency.toUpperCase();
toCurrency = toCurrency.toUpperCase();
if (fromCurrency !== 'EUR' && !cexData[fromCurrency]) {
msg.resolve('This currency is currently not supported.');
return true;
}
if (toCurrency !== 'EUR' && !cexData[toCurrency]) {
msg.resolve('This currency is currently not supported.');
return true;
}
if (fromCurrency === toCurrency) {
msg.resolve('%f %s', inputValue, fromCurrency);
return true;
}
const resultValue = ((cexData[toCurrency] * inputValue) / cexData[fromCurrency]).toFixed(4);
const conversionRate = (cexData[toCurrency] / cexData[fromCurrency]).toFixed(4);
const conversionRateString = inputValue !== 1 ? `(1 = ${conversionRate})` : '';
msg.resolve(`${inputValue} ${fromCurrency} => ${resultValue} ${toCurrency} ${conversionRateString}`);
return true;
},
description: 'Convert between currencies.',
usage: '<number> | [date | list] [<from currency>] [<to currency>]',
aliases: ['cex', 'exchange'],
});
cmds.push({
name: 'randomnumber',
execute: async (
msg: IMessage,
msr: MessageResolver,
spec: any,
prefix: string,
...simplified: any[]
): Promise<boolean> => {
if (simplified.length < 2) {
msg.resolve('Too few arguments!');
return true;
}
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>]',
aliases: ['rnum', 'rand', 'rng'],
});
// 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,
});
commands.registerCommand(
cmds.map((x: any) => {
x.plugin = plugin.manifest.name;
return x;
})
);
}
@Configurable({
ipfsGateway: 'https://ipfs.io',
randomMax: 64,
})
class UtilityPlugin extends Plugin {
public cexCache: { [key: string]: number | CEXResponse } = {
expiry: 0,
date: 0,
cache: {},
};
@DependencyLoad('simplecommands')
addCommands(cmd: any): void {
addCommands(this, cmd);
}
@EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
wipeCaches();
this.cexCache.expiry = 0;
this.cexCache.date = 0;
this.cexCache.cache = {};
this.emit('pluginUnloaded', this);
}
}
initialize(): void {
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();
}
}
module.exports = UtilityPlugin;