plugins-evert/utility/plugin.ts

820 lines
22 KiB
TypeScript

import path from 'path';
import net from 'net';
import cprog from 'child_process';
import convert from 'convert-units';
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';
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): 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 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 RGBToHSL(r: number, g: number, b: number): {[key: string]: number} | null {
// Make r, g, and b fractions of 1
r /= 255;
g /= 255;
b /= 255;
// Find greatest and smallest channel values
const cmin = Math.min(r, g, b);
const cmax = Math.max(r, g, b);
const delta = cmax - cmin;
let h = 0;
let s = 0;
let l = 0;
// Calculate hue
// No difference
if (delta === 0) {
h = 0;
} // Red is max
else if (cmax === r) {
h = ((g - b) / delta) % 6;
} // Green is max
else if (cmax === g) {
h = (b - r) / delta + 2;
} // Blue is max
else {
h = (r - g) / delta + 4;
}
h = Math.round(h * 60);
// Make negative hues positive behind 360°
if (h < 0) {
h += 360;
}
// Calculate lightness
l = (cmax + cmin) / 2;
// Calculate saturation
s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
// Multiply l and s by 100
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
return { h, s, l };
}
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<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> => {
let response = '';
let strArr;
let i;
let text = msg.text.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: '<ENCODE/DECODE> <message>'
});
cmds.push({
name: 'hexstr',
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
let response = '';
let i;
let text = msg.text.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: '<ENCODE/DECODE> <string>'
});
cmds.push({
name: 'base64',
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
let response = '';
const text = msg.text.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: '<ENCODE/DECODE> <string>'
});
cmds.push({
name: 'numsys',
execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
if (simplified.length < 3) {
msg.resolve('Too few arguments!');
return true;
}
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: '<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) {
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) {
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;
}
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: '<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 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;
}
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: '<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']
});
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) {
this.config.save().then(() =>
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]);
}
});
}
}
module.exports = UtilityPlugin;