diff --git a/utility/convert.ts b/utility/convert.ts new file mode 100644 index 0000000..373cf6d --- /dev/null +++ b/utility/convert.ts @@ -0,0 +1,275 @@ +import configureMeasurements, { allMeasures } from 'convert-units'; + +export const convert = configureMeasurements(allMeasures); + +export const bases: {[key: number]: string[]} = { + 2: ['bin', 'binary'], + 8: ['oct', 'octal'], + 10: ['dec', 'decimal'], + 16: ['hex', 'hexadecimal'] +}; + +export 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); +} + +export 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); +} + +export function RGBToHSL(r: number, g: number, b: number): {[key: string]: number} { + // 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 }; +} + +export 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; +} + +// Create an object to efficiently translate unit names to their abbreviations +let unitIndex: { [x: string]: string[] } = {}; +let backupLowercaseUnitTable: { [x: string]: string } = {}; +export function createUnitIndex(): void { + const fullList = convert().list(); + fullList.forEach((unit) => { + const { abbr, singular, plural } = unit; + if (!unitIndex[abbr]) { + unitIndex[abbr] = [ + abbr, singular.toLowerCase(), plural.toLowerCase() + ]; + + // liter <-> litres + if (abbr === 'l') { + unitIndex[abbr].push('liter', 'liters'); + } + + // meter -> metre + if (singular.includes('meter')) { + unitIndex[abbr].push( + singular.replace('meter', 'metre').toLowerCase(), + plural.replace('meter', 'metre').toLowerCase(), + ); + } + + // metre -> meter + if (singular.includes('metre')) { + unitIndex[abbr].push( + singular.replace('metre', 'meter').toLowerCase(), + plural.replace('metre', 'meter').toLowerCase(), + ); + } + + // " per " -> "/" + const appendages: string[] = []; + unitIndex[abbr].forEach((entry) => { + if (entry.includes(' per ')) { + appendages.push(entry.replace(' per ', '/')); + } + }); + unitIndex[abbr].push(...appendages); + + backupLowercaseUnitTable[abbr.toLowerCase()] = abbr; + } + }); +} + +// Get a unit by it's full name or abbreviation +export function getUnitAbbreviation(fullText: string): string | null { + // Regular abbreviation + if (unitIndex[fullText]) { + return fullText; + } + + // Lowercase abbreviation + const lowerText = fullText.toLowerCase(); + if (backupLowercaseUnitTable[lowerText]) { + return backupLowercaseUnitTable[lowerText]; + } + + // Text search + const found = Object.keys(unitIndex).reduce((previous, current) => { + const arrayList = unitIndex[current]; + if (arrayList.includes(lowerText)) { + return current; + } + return previous; + }, ''); + + return found || null; +} + +export function wipeCaches(): void { + unitIndex = {}; + backupLowercaseUnitTable = {}; +} + +export function ASCIIBinaryConverter(input: string, simplified: string[]): string { + let response = ''; + let strArr; + let i; + let text = input.split(' ').slice(2).join(' '); + + 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 = response.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); + } + + return response; +} + +export function ASCIIHexConverter(input: string, simplified: string[]): string { + let response = ''; + let i; + let text = input.split(' ').slice(2).join(' '); + + 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 = 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; + } + + return response; +} + +export function base64Converter(input: string, simplified: string[]): string { + let response = ''; + const text = input.split(' ').slice(2).join(' '); + + switch (simplified[0] ? simplified[0].toUpperCase() : null) { + case 'DECODE': + response = (Buffer.from(text, 'base64').toString('ascii')).replace(/\n/g, '\\n').replace(/\r/g, '\\r'); + break; + case 'ENCODE': + response = Buffer.from(text).toString('base64'); + break; + } + + return response; +} + +export function baseConverter(simplified: string[]): string { + if (simplified.length < 3) { + throw new Error('Too few arguments!'); + } + + 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) { + throw new Error('Invalid conversion.'); + } + + const decimal = parseInt(input, srcBase); + return decimal.toString(dstBase); +} diff --git a/utility/plugin.ts b/utility/plugin.ts index fa86824..8bde27e 100644 --- a/utility/plugin.ts +++ b/utility/plugin.ts @@ -1,7 +1,6 @@ import path from 'path'; import net from 'net'; -import cprog from 'child_process'; -import configureMeasurements, { allMeasures } from 'convert-units'; +import { fork } from 'child_process'; import { Plugin, @@ -14,52 +13,28 @@ 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}; +type CEXResponse = { [key: string]: number }; -const convert = configureMeasurements(allMeasures); - -const cexCache: {[key: string]: number | CEXResponse} = { +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 { return new Promise((resolve, reject) => { @@ -110,75 +85,6 @@ function opMath(expression: string): Promise { }); } -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 { return new Promise((resolve, reject) => { let isFinished = false; @@ -224,119 +130,18 @@ function pingTcpServer(host: string, port: number): Promise { }); } -// Create an object to efficiently translate unit names to their abbreviations -const unitIndex: { [x: string]: string[] } = {}; -const backupLowercaseUnitTable: { [x: string]: string } = {}; -function createUnitIndex(): void { - const fullList = convert().list(); - fullList.forEach((unit) => { - const { abbr, singular, plural } = unit; - if (!unitIndex[abbr]) { - unitIndex[abbr] = [ - abbr, singular.toLowerCase(), plural.toLowerCase() - ]; - - // liter <-> litres - if (abbr === 'l') { - unitIndex[abbr].push('liter', 'liters'); - } - - // meter -> metre - if (singular.includes('meter')) { - unitIndex[abbr].push( - singular.replace('meter', 'metre').toLowerCase(), - plural.replace('meter', 'metre').toLowerCase(), - ); - } - - // metre -> meter - if (singular.includes('metre')) { - unitIndex[abbr].push( - singular.replace('metre', 'meter').toLowerCase(), - plural.replace('metre', 'meter').toLowerCase(), - ); - } - - // " per " -> "/" - const appendages: string[] = []; - unitIndex[abbr].forEach((entry) => { - if (entry.includes(' per ')) { - appendages.push(entry.replace(' per ', '/')); - } - }); - unitIndex[abbr].push(...appendages); - - backupLowercaseUnitTable[abbr.toLowerCase()] = abbr; - } - }); -} - -// Get a unit by it's full name or abbreviation -function getUnitAbbreviation(fullText: string): string | null { - // Regular abbreviation - if (unitIndex[fullText]) { - return fullText; - } - - // Lowercase abbreviation - const lowerText = fullText.toLowerCase(); - if (backupLowercaseUnitTable[lowerText]) { - return backupLowercaseUnitTable[lowerText]; - } - - // Text search - const found = Object.keys(unitIndex).reduce((previous, current) => { - const arrayList = unitIndex[current]; - if (arrayList.includes(lowerText)) { - return current; - } - return previous; - }, ''); - - return found || null; -} - 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 => { - 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; + const result = ASCIIBinaryConverter(msg.text, simplified); + msg.resolve(`> ${result}`); + } catch (e: any) { + msg.resolve('Failed to convert.'); } - - msg.resolve(response); return true; }, description: 'Encode/decode binary (ASCII only)', @@ -346,33 +151,12 @@ function addCommands(plugin: UtilityPlugin, commands: any): void { cmds.push({ name: 'hexstr', execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise => { - 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; + const result = ASCIIHexConverter(msg.text, simplified); + msg.resolve(`> ${result}`); + } catch (e: any) { + msg.resolve('Failed to convert.'); } - - msg.resolve(response); return true; }, description: 'Encode/decode hexadecimal (ASCII only)', @@ -382,24 +166,12 @@ function addCommands(plugin: UtilityPlugin, commands: any): void { cmds.push({ name: 'base64', execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise => { - 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; + const result = base64Converter(msg.text, simplified); + msg.resolve(`> ${result}`); + } catch (e: any) { + msg.resolve('Failed to convert.'); } - - msg.resolve(response); return true; }, description: 'Encode/decode base64 (ASCII only)', @@ -409,27 +181,12 @@ function addCommands(plugin: UtilityPlugin, commands: any): void { cmds.push({ name: 'numsys', execute: async (msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[]): Promise => { - if (simplified.length < 3) { - msg.resolve('Too few arguments!'); - return true; + try { + const result = baseConverter(simplified); + msg.resolve(`> ${result}`); + } catch (e: any) { + msg.resolve('Failed to convert.'); } - - 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.', @@ -458,7 +215,7 @@ function addCommands(plugin: UtilityPlugin, commands: any): void { return true; } - msg.resolve(parseTimeToSeconds(str) + ' seconds'); + msg.resolve(parseTimeToSeconds(str), 'seconds'); return true; }, description: 'Convert ywdhms to seconds.', @@ -598,7 +355,7 @@ function addCommands(plugin: UtilityPlugin, commands: any): void { } const hsl = RGBToHSL(r, g, b); - msg.resolve('hsl(%d, %d%, %d%)', hsl?.h, hsl?.s, hsl?.l); + msg.resolve('hsl(%d, %d%, %d%)', hsl.h, hsl.s, hsl.l); return true; }, description: 'Convert RGB to HSL colors', @@ -894,6 +651,7 @@ class UtilityPlugin extends Plugin { @EventListener('pluginUnload') public unloadEventHandler(plugin: string | Plugin): void { if (plugin === this.name || plugin === this) { + wipeCaches(); this.emit('pluginUnloaded', this); } }