commit bafdffa5708a37f1b11f4e8563c771008b6f7582
parent a38e1b5fb46f8a773756b40b8a901643cf738fb2
Author: Demonstrandum <moi@knutsen.co>
Date: Wed, 18 Mar 2020 15:11:10 +0000
Merge branch 'danyisill-master'
Diffstat:
4 files changed, 632 insertions(+), 620 deletions(-)
diff --git a/lib/commands/help.ts b/lib/commands/help.ts
@@ -0,0 +1,39 @@
+export default home_scope => {
+ const { message, args, HELP_SECTIONS,
+ KNOWN_COMMANDS, CONFIG, ALL_HELP,
+ HELP_KEY, HELP_SOURCE } = home_scope;
+
+ if (args.length === 0 || args[0] == 'help') {
+ message.channel.send(HELP_SECTIONS[0]);
+ return;
+ }
+
+ if (args[0] === 'key') {
+ message.channel.send(HELP_KEY);
+ return;
+ } else if (args[0] === 'source') {
+ message.channel.send(HELP_SOURCE);
+ return;
+ } else if (args[0] === 'all') {
+ for (const msg of ALL_HELP)
+ message.channel.send(msg);
+ return;
+ }
+
+ // Assume the user is now asking for help with a command:
+ // Sanitise:
+ let command = args[0].trim();
+ if (command.head() === CONFIG.commands.prefix)
+ command = command.tail();
+ if (CONFIG.commands.aliases.hasOwnProperty(command))
+ command = CONFIG.commands.aliases[command].trim().squeeze();
+ command = command.split(' ').head().trim().squeeze();
+
+ const help_index = KNOWN_COMMANDS.indexOf(command);
+
+ if (help_index === -1)
+ message.answer(`No such command/help-page (\`${command}\`).`);
+ else
+ message.answer(`**Help (\`${command}\`):**\n`
+ + HELP_SECTIONS[help_index].trim());
+}
diff --git a/lib/commands/weather.ts b/lib/commands/weather.ts
@@ -0,0 +1,31 @@
+import request from 'request';
+
+const WEATHER_URL = 'http://api.openweathermap.org/data/2.5/weather';
+
+export default home_scope => {
+ const { message, args, SECRETS, CONFIG } = home_scope;
+
+ if (args[0] === 'set'){
+ CONFIG.weather_locations[message.author.id] = args.slice(1).join(' ');
+ message.answer(`Your weather location has \
+ been set to ${args.slice(1).join(' ')}`.squeeze());
+ } else {
+ const location = args[0]
+ ? args.join(' ')
+ : CONFIG.weather_locations[message.author.id] || 'Cuckfield';
+ const key = SECRETS.openweather.key;
+
+ request(`${WEATHER_URL}?q=${location}&appid=${key}&units=metric`,
+ (_a, _b, c) => {
+ const d = JSON.parse(c);
+ const date = new Date();
+ const hour = (24 + date.getUTCHours() + d.timezone) % 24;
+ message.answer(`${hour}:${date.getMinutes()} ${d.name}, \
+ ${d.sys.country}: ${d.main.temp}°C \
+ (feels like ${d.main.feels_like}°C) \
+ ${d.weather[0].description}, \
+ ${d.main.temp_max}°C max, \
+ ${d.main.temp_min}°C min`.squeeze());
+ })
+ }
+};
diff --git a/lib/extensions.ts b/lib/extensions.ts
@@ -71,8 +71,8 @@ Array.prototype.mut_map = function (f) {
// String Extensions:
String.prototype.squeeze = function () {
return this
- .replace(/[ ]+/g, ' ')
- .replace(/\n[ ]/g, '\n');
+ .replace(/[\t\s]+/g, ' ')
+ .replace(/\n[\t\s]/g, '\n');
};
String.prototype.leading_space = function () {
diff --git a/lib/main.ts b/lib/main.ts
@@ -7,16 +7,16 @@ import { Message, Attachment, TextChannel } from 'discord.js';
// System interaction modules.
import {
- readFileSync as read_file,
- writeFileSync as write_file,
+ readFileSync as read_file,
+ writeFileSync as write_file,
+ readdirSync as read_dir
} from 'fs';
import { execSync as shell } from 'child_process';
-import request from 'request';
// Local misc/utility functions.
import './extensions';
import { deep_merge, pp, compile_match,
- export_config, access, glue_strings } from './utils';
+ export_config, access, glue_strings } from './utils';
import format_oed from './format_oed'; // O.E.D. JSON entry to markdown.
// Default bot configuration JSON.
@@ -28,22 +28,22 @@ import oed_lookup from './api/oxford';
import urban_search from './api/urban';
import yt_search from './api/yt_scrape';
import { pastebin_latest,
- pastebin_update,
- pastebin_url } from './api/pastebin';
+ pastebin_update,
+ pastebin_url } from './api/pastebin';
// Anything that hasn't been defined in `bot.json`
// will be taken care of by the defaults.
const CONFIG = deep_merge(
- DEFAULT_CONFIG,
- JSON.parse(read_file('./bot.json', 'utf-8')));
+ DEFAULT_CONFIG,
+ JSON.parse(read_file('./bot.json', 'utf-8')));
// CONFIG will eventually update to the online version.
pastebin_latest().then(res =>
- deep_merge(CONFIG, res)).catch(console.log);
+ deep_merge(CONFIG, res)).catch(console.log);
// Precompile all regular-expressions in known places.
['respond', 'reject', 'replace']
- .each(name => CONFIG.rules[name].mut_map(compile_match))
+ .each(name => CONFIG.rules[name].mut_map(compile_match))
// Store secrets in an object, retrieved from shell's
// environment variables.
@@ -52,638 +52,580 @@ const SECRETS = JSON.parse(shell('sh ./generate_secrets.sh').toString());
// Load HELP.md file, and split text smart-ly
// (to fit within 2000 characters).
const [HELP_KEY, HELP, HELP_SOURCE] = read_file('./HELP.md')
- .toString().split('▬▬▬');
+ .toString().split('▬▬▬');
const HELP_SECTIONS = HELP.toString()
- .replace(/\n -/g, '\n \u25b8')
- .replace(/\n- /g, '@@@\n\u2b25 ')
- .split('@@@')
- .filter(e => !!e && !!e.trim());
+ .replace(/\n -/g, '\n \u25b8')
+ .replace(/\n- /g, '@@@\n\u2b25 ')
+ .split('@@@')
+ .filter(e => !!e && !!e.trim());
// This assumes no two help-entries would ever
// be greater than 2000 characters long
const HELP_MESSAGES = glue_strings(HELP_SECTIONS);
const ALL_HELP = glue_strings([
- HELP_KEY,
- '\n▬▬▬\n', ...HELP_MESSAGES,
- '\n▬▬▬\n', HELP_SOURCE
+ HELP_KEY,
+ '\n▬▬▬\n', ...HELP_MESSAGES,
+ '\n▬▬▬\n', HELP_SOURCE
]);
const KNOWN_COMMANDS = HELP_SECTIONS.map(e =>
- e.slice(5).replace(/(\s.*)|(`.*)/g, ''));
+ e.slice(5).replace(/(\s.*)|(`.*)/g, ''));
const GIT_URL = 'https://github.com/Demonstrandum/Simp-O-Matic';
-const WEATHER_URL = 'http://api.openweathermap.org/data/2.5/weather';
+const HOMESCOPE = {
+ HELP_SOURCE, HELP_KEY, GIT_URL,
+ HELP_MESSAGES, HELP_SECTIONS, ALL_HELP,
+ CONFIG, SECRETS, KNOWN_COMMANDS
+};
// Log where __dirname and cwd are for deployment.
console.log('File/Execution locations:', {
- '__dirname': __dirname,
- 'process.cwd()': process.cwd()
+ '__dirname': __dirname,
+ 'process.cwd()': process.cwd()
});
@Discord
export class SimpOMatic {
- private static _client : Client;
- private _COMMAND_HISTORY : Message[] = [];
-
- constructor() {
- console.log('Secrets:', pp(SECRETS));
- console.log('Configured Variables:', pp(CONFIG));
- console.log('Known commands:', pp(KNOWN_COMMANDS));
- }
-
- static start() {
- this._client = new Client();
- this._client.login(
- SECRETS.api.token,
- `${__dirname}/*Discord.ts`
- );
- }
-
- process_command(message : Message) {
- const last_command = this._COMMAND_HISTORY.last();
- this._COMMAND_HISTORY.push(message);
- if (this._COMMAND_HISTORY.length > CONFIG.commands.max_history) {
- this._COMMAND_HISTORY.shift();
- }
- const current_command = this._COMMAND_HISTORY.last();
-
- // Try and slow the fellas down a little.
- if (!!last_command
- && last_command.channel === current_command.channel
- && last_command.author.id === current_command.author.id) {
- // Only give spam warning if commands are coming
- // in fast _in the same channel_ and by the same user.
- const delta = current_command.createdTimestamp - last_command.createdTimestamp;
- if (last_command.content === current_command.content
- && delta <= 1400) {
- if (delta <= 400) return;
- return message.answer(`I can't help but notice you're running \
- the same commands over in rather rapid succession.
- Would you like to slow down a little?`.squeeze())
- }
- if (delta <= 900) {
- if (delta <= 300) return;
- return message.answer('Slow down there bucko.');
- }
- }
-
- const content = message.content.trim().squeeze();
- const words = content.tail().split(' ');
- const args = words.tail();
-
- let command = words[0].toLowerCase();
- if (CONFIG.commands.aliases.hasOwnProperty(command))
- command = CONFIG.commands.aliases[command].trim().squeeze();
-
- const expanded_command_words = command.split(' ');
- if (expanded_command_words.length > 1) {
- // This means the alias has expanded to more than just one word.
- command = expanded_command_words.shift();
- expanded_command_words.each(e => args.unshift(e));
- }
-
- command = command.toLowerCase();
- console.log('Received command:', [command, args]);
-
- switch (command) {
- case 'ping': {
- message.answer("PONGGERS!");
- break;
- } case 'help': {
- if (args.length === 0 || args[0] == 'help') {
- message.channel.send(HELP_SECTIONS[0]);
- break;
- }
-
- if (args[0] === 'key') {
- message.channel.send(HELP_KEY);
- break;
- } else if (args[0] === 'source') {
- message.channel.send(HELP_SOURCE);
- break;
- } else if (args[0] === 'all') {
- for (const msg of ALL_HELP)
- message.channel.send(msg);
- break;
- }
-
- // Assume the user is now asking for help with a command:
- // Sanitise:
- let command = args[0].trim();
- if (command.head() === CONFIG.commands.prefix)
- command = command.tail();
- if (CONFIG.commands.aliases.hasOwnProperty(command))
- command = CONFIG.commands.aliases[command].trim().squeeze();
- command = command.split(' ').head().trim().squeeze();
-
- const help_index = KNOWN_COMMANDS.indexOf(command);
-
- if (help_index === -1)
- message.answer(`No such command/help-page (\`${command}\`).`);
- else
- message.answer(`**Help (\`${command}\`):**\n`
- + HELP_SECTIONS[help_index].trim());
-
- break;
- } case 'commands': {
- const p = CONFIG.commands.prefix;
- const joined_commands = KNOWN_COMMANDS.slice(0, -1)
- .map(c => `\`${p}${c}\``)
- .join(', ');
- const last_command = `\`${p}${KNOWN_COMMANDS.last()}\``;
- message.reply(`All known commands (excluding aliases): \
- ${joined_commands} and ${last_command}`.squeeze());
- break;
- } case 'id': {
- if (args[0]) {
- const matches = args[0].match(/<@!?(\d+)>/)
- if (!matches) {
- message.answer(`Please tag a user, or \
- provide no argument(s) at all. See \`!help id\``
- .squeeze());
- } else {
- message.answer(`User ID: \`${matches[1]}\``);
- }
- break;
- }
- const reply = `User ID: \`${message.author.id}\`
- Author: ${message.author}
- Message ID: \`${message.id}\``.squeeze();
- console.log(`Replied: ${reply}`);
- message.answer(reply);
- break;
- } case 'get': {
- if (args.length === 0) {
- message.answer('To view the entire object, use the `!export` command.');
- break;
- }
- // Accessing invalid fields will be caught.
- try {
- const accessors = args[0].trim().split('.').squeeze();
- const resolution = access(CONFIG, accessors);
- message.channel.send(` ⇒ \`${resolution}\``);
- } catch (e) {
- message.channel.send(`Invalid object access-path\n`
- + `Problem: \`\`\`\n${e}\n\`\`\``);
- }
- break;
- } case 'set': {
- if (args.length < 2) {
- message.answer('Please provide two arguments.\nSee `!help set`.');
- break;
- }
- try {
- const accessors = args[0].trim().split('.').squeeze();
- const parent = accessors.pop();
- const obj = access(CONFIG, accessors);
- obj[parent] = JSON.parse(args[1]);
-
- message.channel.send(`Assignment successful.
- \`${args[0].trim()} = ${obj[parent]}\``.squeeze());
- } catch (e) {
- message.channel.send(`Invalid object access-path,`
- + `nothing set.\nProblem: \`\`\`\n${e}\n\`\`\``);
- }
- break;
- } case 'alias': {
- const p = CONFIG.commands.prefix;
-
- if (args.length === 0 || args[0] === 'ls') {
- const lines = Object.keys(CONFIG.commands.aliases)
- .map((e, i) => `${i + 1}. \`${p}${e}\` ↦ \`${p}${CONFIG.commands.aliases[e]}\``);
- message.answer('List of **Aliases**:\n')
- message.channel.send('**KEY: `Alias` ↦ `Command it maps to`**\n\n'
- + lines.join('\n'));
- break;
- }
-
- // Parse `!alias rm` command.
- if (args[0] === 'rm' && args.length > 1) {
- const aliases = CONFIG.commands.aliases;
- const keys = Object.keys(aliases);
- let match, index, alias;
- if (match = args[1].match(/^#?(\d+)/)) {
- index = Number(match[1]) - 1;
- if (index >= keys.length) {
- message.answer('No alias exists at such an index'
- + ` (there are only ${keys.length} indices).`);
- break;
- }
- alias = keys[index];
- keys.each((_, i) => i === index
- ? delete aliases[alias]
- : null);
- } else {
- alias = args[1];
- if (alias[0] === '!') alias = alias.tail();
- index = keys.indexOf(alias);
- if (index === -1) {
- message.answer(`There does not exist any alias \
- with the name \`${p}${alias}\`.`.squeeze());
- break;
- }
- keys.each((a, _) => a === alias
- ? delete alias[alias]
- : null);
- }
- message.answer(`Alias \`${p}${alias}\` at index \
- number ${index + 1}, has been deleted.`.squeeze());
- break;
- }
-
- // Check last:
- if (args.length > 1) { // Actually aliasing something.
- args[0] = args[0].trim();
- args[1] = args[1].trim();
-
- if (args[0][0] === CONFIG.commands.prefix)
- args[0] = args[0].tail();
-
- if (args[1][0] === CONFIG.commands.prefix)
- args[1] = args[1].tail();
-
- CONFIG.commands.aliases[args[0]] = args.tail().join(' ');
- message.channel.send(
- '**Alias added:**\n >>> ' +
- `\`${p}${args[0]}\` now maps to \`${p}${args.tail().join(' ')}\``);
- } else {
- if (args.length === 1) {
- if (args[0] in CONFIG.commands.aliases) {
- const aliases = Object.keys(CONFIG.commands.aliases);
- const n = aliases.indexOf(args[0]) + 1;
- message.answer(`${n}. \`${p}${args[0]}\` ↦ \`${p}${CONFIG.commands.aliases[args[0]]}\``);
- break;
- } else {
- message.answer('No such alias found.');
- break;
- }
- }
- message.answer('Invalid number of arguments to alias,\n'
- + 'Please see `!help alias`.');
- }
- break;
- } case 'prefix': {
- if (args.length == 1) {
- if (args[0].length !== 1) {
- message.answer(`You may only use a prefix that is
- exactly one character/symbol/grapheme/rune long.`
- .squeeze());
- break;
- }
- CONFIG.commands.prefix = args[0];
- message.answer(`Command prefix changed to: \`${CONFIG.commands.prefix}\`.`);
- break;
- }
- message.answer(`Current command prefix is: \`${CONFIG.commands.prefix}\`.`);
- break;
- } case 'ignore': {
- // Man alive, someone else do this please.
- break;
- } case 'response': {
- // TODO
- break
- } case 'search': {
- const query = args.join(' ').toLowerCase();
-
- web_search({
- kind: 'web',
- query,
- key: SECRETS.google.api_key,
- id: SECRETS.google.search_id
- }).then((res) => message.answer(res))
- .catch(e => message.answer(e));
- break;
- } case 'image': {
- const query = args.join(' ').toLowerCase();
-
- web_search({
- kind: 'image',
- query,
- key: SECRETS.google.api_key,
- id: SECRETS.google.search_id
- }).then(res => message.answer(res))
- .catch(er => message.answer(er));
- break;
- } case 'youtube': {
- const query = args.join(' ');
- yt_search({ query })
- .then(message.reply.bind(message))
- .catch(message.answer.bind(message));
- break;
- } case 'define': {
- message.answer('Looking in the Oxford English Dictionary...');
- const query = args.join(' ');
-
- const nasty_reply = `Your word (‘${query}’) is nonsense, either \
- that or they've forgotten to index it.
- I'll let you decide.
-
- P.S. Try the _Urban Dictionary_ \
- (\`!urban ${query}\`)`.squeeze();
-
- oed_lookup({
- word: query,
- lang: CONFIG.lang,
- id: SECRETS.oxford.id,
- key: SECRETS.oxford.key
- }).then(res => {
- console.log('Dictionary response:', pp(res));
- if (!res['results']
- || res['results'].length == 0
- || !res['results'][0].lexicalEntries
- || res['results'][0].lexicalEntries.length == 0
- || res['results'][0].lexicalEntries[0].entries.length == 0
- || res['results'][0].lexicalEntries[0].entries[0].senses.length == 0) {
- message.answer(nasty_reply);
- return;
- }
- // Format the dictionary entry as a string.
- const msg = format_oed(res, message);
-
- if (msg.length >= 2000) { // This should be rare (try defining `run').
- let part_msg = "";
- // This assumes no two lines would ever
- // amount to more than 2000 characters.
- for (const line of msg.split(/\n/g))
- if (part_msg.length + line.length >= 2000) {
- message.channel.send(part_msg);
- part_msg = line + '\n';
- } else { part_msg += line + '\n' }
- // Send what's left over, and not >2000 characters.
- message.channel.send(part_msg);
-
- return;
- }
- message.channel.send(msg);
- }).catch(e => {
- if (e.status == 404) {
- message.channel.send(`That 404'd. ${nasty_reply}`);
- } else {
- message.channel.send(`Error getting definition:\n${e}`);
- }
- });
- break;
- } case 'urban': {
- const query = args.join(' ');
- message.answer('Searching Urban Dictionary...');
- urban_search({ query, key: SECRETS.rapid.key }).then(res => {
- if (res['list'].length === 0) {
- message.channel.send(`Congratulations, not even Urban \
- Dictionary knows what you're trying to say.`.squeeze());
- return;
- }
- const entry = res['list'][0];
- const def = entry.definition.replace(/\[|\]/g, '');
-
- message.channel.send(`**Urban Dictionary** defines \
- ‘${query}’, as:\n>>> ${def.trim()}`.squeeze());
-
- let example = entry.example;
- if (!!example || example.length > 0) {
- example = example.replace(/\[|\]/g, '');
- message.channel.send(`\n**Example**:\n>>> ${example.trim()}`);
- }
- message.channel.send(`Link: ${entry.permalink}`);
- }).catch(e => message.answer(`Error fetching definition:\n${e}`));
- break;
- } case 'milkies': {
- message.answer(`${(4 + Math.random() * 15).round_to(3)} gallons \
- of milkies have been deposited in your mouth.`.squeeze());
- break;
- } case 'ily': {
- message.answer('Y-you too...');
- break;
- } case 'say': {
- message.answer(`Me-sa says: “${args.join(' ')}”`);
- break;
- } case 'invite': {
- message.answer('Invite link: https://discordapp.com/api/oauth2/authorize?client_id=684895962212204748&permissions=8&scope=bot');
- break;
- } case 'export': {
- let export_string = export_config(CONFIG, {});
- if (export_string.length > 1980) {
- export_string = export_config(CONFIG, { ugly: true });
- }
-
- const today = (new Date())
- .toISOString()
- .replace(/\..*/, '')
- .split('T')
- .reverse()
- .join('_');
-
- const file_name = `export-${today}.json`
- const file_dest = `${process.cwd()}/${file_name}`;
- write_file(file_dest, export_config(CONFIG, {}));
- pastebin_update(export_config(CONFIG, {}));
-
- if (export_string.length < 1980) {
- message.channel.send("```json\n" + export_string + "\n```");
- }
- const attach = new Attachment(file_dest, file_name);
- message.channel.send("**Export:**", attach);
-
- message.answer(`A copy of this export (\`export-${today}.json\`) \
- has been saved to the local file system.
- Pastebin file: ${pastebin_url}`.squeeze());
- break;
- } case 'weather': {
- // Thanks to Daniel (Danny) for the weather API code!
- // -> https://github.com/danyisill
- const locations = CONFIG.weather_locations;
- if (args[0] === 'set') {
- locations[message.author.id] = args[1];
- message.answer(`Your weather location has \
- been set to ${args[1]}`.squeeze());
- } else {
- const location = args[0]
- || locations[message.author.id]
- || 'Cuckfield';
-
- const key = SECRETS.openweather.key;
- request(`${WEATHER_URL}?q=${location}&appid=${key}`,
- (_a, _b, c : string) => {
- let d = JSON.parse(c);
- const date = new Date();
- const hour = Math.abs(date.getUTCHours() + d.timezone) % 24;
- const min = date.getMinutes();
- const temp = Math.round(d.main.temp - 273.15);
- const feels = Math.round(d.main.feels_like - 273.15);
- const max_temp = Math.round(d.main.temp_max - 273.15);
- const min_temp = Math.round(d.main.temp_min - 273.15);
- message.answer(`${hour}:${min} ${d.name}, \
- ${d.sys.country}: ${temp}°C \
- (feels like ${feels}°C) \
- ${d.weather[0].description}, \
- ${max_temp}°C max, \
- ${min_temp}°C min`.squeeze());
- });
- }
- break;
- } case 'ls': {
- const dirs = {
- '__dirname': __dirname,
- 'process.cwd()': process.cwd()
- };
- message.channel.send(`Directories:\n\`\`\`json\n${dirs}\n\`\`\``);
- break;
- } case 'github': {
- message.answer(`${GIT_URL}/`);
- break;
- } case 'fork': {
- message.answer(`${GIT_URL}/fork`)
- break;
- } case 'issue': {
- message.answer(`${GIT_URL}/issues`)
- break;
- } case '': {
- message.answer("That's an empty command...");
- break;
- } default: {
- if (KNOWN_COMMANDS.includes(command)) {
- const p = CONFIG.commands.prefix;
- message.reply(`:scream: *gasp!* — The \`${p}${command}\` \
- command has not been implemented yet. \
- Quick send a pull request! Just type in \
- \`${p}fork\`, and get started...`.squeeze());
- break;
- }
- message.answer(`
- :warning: ${CONFIG.commands.not_understood}.
- > \`${CONFIG.commands.prefix}${command}\``.squeeze());
- break;
- }
- }
- }
-
- process_generic(message : Message) {
- const { content } = message;
- if (content.includes('bot'))
- message.answer("The hell you sayn' about bots?");
- // TODO: Process _rules_ appropriately.
- }
-
- async last_message(opts) : Promise<string> {
- const channel = opts.channel as TextChannel;
-
- if (!opts.offset) opts.offset = 1;
- if (opts.mention) opts.mentioning = opts.mentioning.trim();
-
- if (opts.command) {
- let commands = this._COMMAND_HISTORY
- .filter(m => m.channel.id === channel.id)
-
- if (opts.mention) commands = commands.filter(m =>
- m.author.toString() === opts.mentioning);
-
- const command = commands.last(opts.offset - 1);
- if (!command) {
- channel.send('Cannot expand, no such'
- + ' command exists in history.');
- return Promise.reject('COMMAND_NOT_IN_HISTORY');
- }
- return Promise.resolve(command.content);
- }
-
- let filter = _ => true;
- if (opts.mention)
- filter = m => m.author.toString() === opts.mentioning;
-
- const messages = await channel.fetchMessages({
- limit: CONFIG.commands.max_history
- });
- // Remember that the _latest_ message, is the one that
- // the user has _just_ sent. This means we ignore the first message.
- return messages.array()
- .filter(filter)
- .get(opts.offset).content;
- }
-
- async expand(message : Message) : Promise<string> {
- // History expansion with !!, !!@, !!^@, !!<ordinal>, etc.
- const expansions = message.content
- .replace(/(!!@?\^?\d*)/g, '@EXP$1@EXP')
- .replace(/(\s)/g, '@EXP$1@EXP')
- .split('@EXP').squeeze();
-
- for (let i = 0; i < expansions.length; ++i) {
- if (expansions[i].startsWith('!!')) {
- if (expansions[i].length === 2) { // !! expansion
- expansions[i] = await this.last_message({
- command: true,
- channel: message.channel
- });
- continue;
- }
-
- const [opts, offset] = expansions[i].slice(2)
- .replace(/(\d+)/, '#$1')
- .split('#');
-
- const mention = opts.includes('@');
- const mentioning = expansions[i + 2];
- if (mention) expansions[i + 2] = '';
-
- expansions[i] = await this.last_message({
- command: !opts.includes('^'),
- mention: mention,
- mentioning: mentioning,
- offset: offset || 1,
- channel: message.channel
- });
- }
- }
-
- // TODO: Deal with 'EXPANSION_ERROR's in `expansions`.
-
- return expansions.join('');
- }
-
- @On("message")
- async on_message(message : Message, client : Client) {
- console.log('Message acknowledged.');
- if (SimpOMatic._client.user.id === message.author.id) {
- return;
- }
- console.log('Message received:', message.content);
-
- const trimmed = message.content.trim();
- message.content = trimmed;
- // When finished expanding...
- this.expand(message).then(content => {
- if (content.length >= 2000) {
- message.answer("The expansion for that message was"
- + " over 2000 characters, what the fuck is wrong with you?");
- return;
- }
- message.content = content;
- console.log('Expanded message:', message.content);
-
- if (message.content[0] === CONFIG.commands.prefix) {
- console.log('Message type: command.')
- this.process_command(message);
- } else {
- console.log('Message type: generic.')
- this.process_generic(message);
- }
- });
- }
+ private static _client : Client;
+ private _COMMAND_HISTORY : Message[] = [];
+
+ constructor() {
+ console.log('Secrets:', pp(SECRETS));
+ console.log('Configured Variables:', pp(CONFIG));
+ console.log('Known commands:', pp(KNOWN_COMMANDS));
+ }
+
+ static start() {
+ this._client = new Client();
+ this._client.login(
+ SECRETS.api.token,
+ `${__dirname}/*Discord.ts`
+ );
+ }
+
+ process_command(message : Message) {
+ const last_command = this._COMMAND_HISTORY.last();
+ this._COMMAND_HISTORY.push(message);
+ if (this._COMMAND_HISTORY.length > CONFIG.commands.max_history) {
+ this._COMMAND_HISTORY.shift();
+ }
+ const current_command = this._COMMAND_HISTORY.last();
+
+ // Try and slow the fellas down a little.
+ if (!!last_command
+ && last_command.channel === current_command.channel
+ && last_command.author.id === current_command.author.id) {
+ // Only give spam warning if commands are coming
+ // fast _in the same channel_.
+ const delta = current_command.createdTimestamp - last_command.createdTimestamp;
+ if (last_command.content === current_command.content
+ && delta <= 1400) {
+ if (delta <= 400) return;
+ return message.answer(`I can't help but notice you're running \
+ the same commands over in rather rapid succession.
+ Would you like to slow down a little?`.squeeze())
+ }
+ if (delta <= 900) {
+ if (delta <= 300) return;
+ return message.answer('Slow down there bucko.');
+ }
+ }
+
+ const content = message.content.trim().squeeze();
+ const words = content.tail().split(' ');
+ const args = words.tail();
+
+ let command = words[0].toLowerCase();
+ if (CONFIG.commands.aliases.hasOwnProperty(command))
+ command = CONFIG.commands.aliases[command].trim().squeeze();
+
+ const expanded_command_words = command.split(' ');
+ if (expanded_command_words.length > 1) {
+ // This means the alias has expanded to more than just one word.
+ command = expanded_command_words.shift();
+ expanded_command_words.each(e => args.unshift(e));
+ }
+
+ command = command.toLowerCase();
+ console.log('Received command:', [command, args]);
+
+ // This should have most immediate access.
+ if (command === 'ping') return message.answer('PONGGERS!');
+
+ const commands = read_dir(`${__dirname}/commands`)
+ .map(n => n.slice(0, -3));
+ if (commands.includes(command))
+ return require(`./commands/${command}`)
+ .default({ // Basic 'home-scope' is passed in.
+ message, args, ...HOMESCOPE});
+
+ switch (command) {
+ case 'commands': {
+ const p = CONFIG.commands.prefix;
+ const joined_commands = KNOWN_COMMANDS.slice(0, -1)
+ .map(c => `\`${p}${c}\``)
+ .join(', ');
+ const last_command = `\`${p}${KNOWN_COMMANDS.last()}\``;
+ message.reply(`All known commands (excluding aliases): \
+ ${joined_commands} and ${last_command}`.squeeze());
+ break;
+ } case 'id': {
+ if (args[0]) {
+ const matches = args[0].match(/<@!?(\d+)>/)
+ if (!matches) {
+ message.answer(`Please tag a user, or \
+ provide no argument(s) at all. See \`!help id\``
+ .squeeze());
+ } else {
+ message.answer(`User ID: \`${matches[1]}\``);
+ }
+ break;
+ }
+ const reply = `User ID: \`${message.author.id}\`
+ Author: ${message.author}
+ Message ID: \`${message.id}\``.squeeze();
+ console.log(`Replied: ${reply}`);
+ message.answer(reply);
+ break;
+ } case 'get': {
+ if (args.length === 0) {
+ message.answer('To view the entire object, use the `!export` command.');
+ break;
+ }
+ // Accessing invalid fields will be caught.
+ try {
+ const accessors = args[0].trim().split('.').squeeze();
+ const resolution = access(CONFIG, accessors);
+ message.channel.send(` ⇒ \`${resolution}\``);
+ } catch (e) {
+ message.channel.send(`Invalid object access-path\n`
+ + `Problem: \`\`\`\n${e}\n\`\`\``);
+ }
+ break;
+ } case 'set': {
+ if (args.length < 2) {
+ message.answer('Please provide two arguments.\nSee `!help set`.');
+ break;
+ }
+ try {
+ const accessors = args[0].trim().split('.').squeeze();
+ const parent = accessors.pop();
+ const obj = access(CONFIG, accessors);
+ obj[parent] = JSON.parse(args[1]);
+
+ message.channel.send(`Assignment successful.
+ \`${args[0].trim()} = ${obj[parent]}\``.squeeze());
+ } catch (e) {
+ message.channel.send(`Invalid object access-path,`
+ + `nothing set.\nProblem: \`\`\`\n${e}\n\`\`\``);
+ }
+ break;
+ } case 'alias': {
+ const p = CONFIG.commands.prefix;
+
+ if (args.length === 0 || args[0] === 'ls') {
+ const lines = Object.keys(CONFIG.commands.aliases)
+ .map((e, i) => `${i + 1}. \`${p}${e}\` ↦ \`${p}${CONFIG.commands.aliases[e]}\``);
+ message.answer('List of **Aliases**:\n')
+ message.channel.send('**KEY: `Alias` ↦ `Command it maps to`**\n\n'
+ + lines.join('\n'));
+ break;
+ }
+
+ // Parse `!alias rm` command.
+ if (args[0] === 'rm' && args.length > 1) {
+ const aliases = CONFIG.commands.aliases;
+ const keys = Object.keys(aliases);
+ let match, index, alias;
+ if (match = args[1].match(/^#?(\d+)/)) {
+ index = Number(match[1]) - 1;
+ if (index >= keys.length) {
+ message.answer('No alias exists at such an index'
+ + ` (there are only ${keys.length} indices).`);
+ break;
+ }
+ alias = keys[index];
+ keys.each((_, i) => i === index
+ ? delete aliases[alias]
+ : null);
+ } else {
+ alias = args[1];
+ if (alias[0] === '!') alias = alias.tail();
+ index = keys.indexOf(alias);
+ if (index === -1) {
+ message.answer(`There does not exist any alias \
+ with the name \`${p}${alias}\`.`.squeeze());
+ break;
+ }
+ keys.each((a, _) => a === alias
+ ? delete alias[alias]
+ : null);
+ }
+ message.answer(`Alias \`${p}${alias}\` at index \
+ number ${index + 1}, has been deleted.`.squeeze());
+ break;
+ }
+
+ // Check last:
+ if (args.length > 1) { // Actually aliasing something.
+ args[0] = args[0].trim();
+ args[1] = args[1].trim();
+
+ if (args[0][0] === CONFIG.commands.prefix)
+ args[0] = args[0].tail();
+
+ if (args[1][0] === CONFIG.commands.prefix)
+ args[1] = args[1].tail();
+
+ CONFIG.commands.aliases[args[0]] = args.tail().join(' ');
+ message.channel.send(
+ '**Alias added:**\n >>> ' +
+ `\`${p}${args[0]}\` now maps to \`${p}${args.tail().join(' ')}\``);
+ } else {
+ if (args.length === 1) {
+ if (args[0] in CONFIG.commands.aliases) {
+ const aliases = Object.keys(CONFIG.commands.aliases);
+ const n = aliases.indexOf(args[0]) + 1;
+ message.answer(`${n}. \`${p}${args[0]}\` ↦ \`${p}${CONFIG.commands.aliases[args[0]]}\``);
+ break;
+ } else {
+ message.answer('No such alias found.');
+ break;
+ }
+ }
+ message.answer('Invalid number of arguments to alias,\n'
+ + 'Please see `!help alias`.');
+ }
+ break;
+ } case 'prefix': {
+ if (args.length == 1) {
+ if (args[0].length !== 1) {
+ message.answer(`You may only use a prefix that is
+ exactly one character/symbol/grapheme/rune long.`
+ .squeeze());
+ break;
+ }
+ CONFIG.commands.prefix = args[0];
+ message.answer(`Command prefix changed to: \`${CONFIG.commands.prefix}\`.`);
+ break;
+ }
+ message.answer(`Current command prefix is: \`${CONFIG.commands.prefix}\`.`);
+ break;
+ } case 'ignore': {
+ // Man alive, someone else do this please.
+ break;
+ } case 'response': {
+ // TODO
+ break
+ } case 'search': {
+ const query = args.join(' ').toLowerCase();
+
+ web_search({
+ kind: 'web',
+ query,
+ key: SECRETS.google.api_key,
+ id: SECRETS.google.search_id
+ }).then((res) => message.answer(res))
+ .catch(e => message.answer(e));
+ break;
+ } case 'image': {
+ const query = args.join(' ').toLowerCase();
+
+ web_search({
+ kind: 'image',
+ query,
+ key: SECRETS.google.api_key,
+ id: SECRETS.google.search_id
+ }).then(res => message.answer(res))
+ .catch(er => message.answer(er));
+ break;
+ } case 'youtube': {
+ const query = args.join(' ');
+ yt_search({ query })
+ .then(message.reply.bind(message))
+ .catch(message.answer.bind(message));
+ break;
+ } case 'define': {
+ message.answer('Looking in the Oxford English Dictionary...');
+ const query = args.join(' ');
+
+ const nasty_reply = `Your word (‘${query}’) is nonsense, either \
+ that or they've forgotten to index it.
+ I'll let you decide.
+
+ P.S. Try the _Urban Dictionary_ \
+ (\`!urban ${query}\`)`.squeeze();
+
+ oed_lookup({
+ word: query,
+ lang: CONFIG.lang,
+ id: SECRETS.oxford.id,
+ key: SECRETS.oxford.key
+ }).then(res => {
+ console.log('Dictionary response:', pp(res));
+ if (!res['results']
+ || res['results'].length == 0
+ || !res['results'][0].lexicalEntries
+ || res['results'][0].lexicalEntries.length == 0
+ || res['results'][0].lexicalEntries[0].entries.length == 0
+ || res['results'][0].lexicalEntries[0].entries[0].senses.length == 0) {
+ message.answer(nasty_reply);
+ return;
+ }
+ // Format the dictionary entry as a string.
+ const msg = format_oed(res, message);
+
+ if (msg.length >= 2000) { // This should be rare (try defining `run').
+ let part_msg = "";
+ // This assumes no two lines would ever
+ // amount to more than 2000 characters.
+ for (const line of msg.split(/\n/g))
+ if (part_msg.length + line.length >= 2000) {
+ message.channel.send(part_msg);
+ part_msg = line + '\n';
+ } else { part_msg += line + '\n' }
+ // Send what's left over, and not >2000 characters.
+ message.channel.send(part_msg);
+
+ return;
+ }
+ message.channel.send(msg);
+ }).catch(e => {
+ if (e.status == 404) {
+ message.channel.send(`That 404'd. ${nasty_reply}`);
+ } else {
+ message.channel.send(`Error getting definition:\n${e}`);
+ }
+ });
+ break;
+ } case 'urban': {
+ const query = args.join(' ');
+ message.answer('Searching Urban Dictionary...');
+ urban_search({ query, key: SECRETS.rapid.key }).then(res => {
+ if (res['list'].length === 0) {
+ message.channel.send(`Congratulations, not even Urban \
+ Dictionary knows what you're trying to say.`.squeeze());
+ return;
+ }
+ const entry = res['list'][0];
+ const def = entry.definition.replace(/\[|\]/g, '');
+
+ message.channel.send(`**Urban Dictionary** defines \
+ ‘${query}’, as:\n>>> ${def.trim()}`.squeeze());
+
+ let example = entry.example;
+ if (!!example || example.length > 0) {
+ example = example.replace(/\[|\]/g, '');
+ message.channel.send(`\n**Example**:\n>>> ${example.trim()}`);
+ }
+ message.channel.send(`Link: ${entry.permalink}`);
+ }).catch(e => message.answer(`Error fetching definition:\n${e}`));
+ break;
+ } case 'milkies': {
+ message.answer(`${(4 + Math.random() * 15).round_to(3)} gallons \
+ of milkies have been deposited in your mouth.`.squeeze());
+ break;
+ } case 'ily': {
+ message.answer('Y-you too...');
+ break;
+ } case 'say': {
+ message.answer(`Me-sa says: “${args.join(' ')}”`);
+ break;
+ } case 'invite': {
+ message.answer('Invite link: https://discordapp.com/api/oauth2/authorize?client_id=684895962212204748&permissions=8&scope=bot');
+ break;
+ } case 'export': {
+ let export_string = export_config(CONFIG, {});
+ if (export_string.length > 1980) {
+ export_string = export_config(CONFIG, { ugly: true });
+ }
+
+ const today = (new Date())
+ .toISOString()
+ .replace(/\..*/, '')
+ .split('T')
+ .reverse()
+ .join('_');
+
+ const file_name = `export-${today}.json`
+ const file_dest = `${process.cwd()}/${file_name}`;
+ write_file(file_dest, export_config(CONFIG, {}));
+ pastebin_update(export_config(CONFIG, {}));
+
+ if (export_string.length < 1980) {
+ message.channel.send("```json\n" + export_string + "\n```");
+ }
+ const attach = new Attachment(file_dest, file_name);
+ message.channel.send("**Export:**", attach);
+
+ message.answer(`A copy of this export (\`export-${today}.json\`) \
+ has been saved to the local file system.
+ Pastebin file: ${pastebin_url}`.squeeze());
+ break;
+ } case 'ls': {
+ const dirs = {
+ '__dirname': __dirname,
+ 'process.cwd()': process.cwd()
+ };
+ message.channel.send(`Directories:\n\`\`\`json\n${dirs}\n\`\`\``);
+ break;
+ } case 'github': {
+ message.answer(`${GIT_URL}/`);
+ break;
+ } case 'fork': {
+ message.answer(`${GIT_URL}/fork`)
+ break;
+ } case 'issue': {
+ message.answer(`${GIT_URL}/issues`)
+ break;
+ } case '': {
+ message.answer("That's an empty command...");
+ break;
+ } default: {
+ if (KNOWN_COMMANDS.includes(command)) {
+ const p = CONFIG.commands.prefix;
+ message.reply(`:scream: *gasp!* — The \`${p}${command}\` \
+ command has not been implemented yet. \
+ Quick send a pull request! Just type in \
+ \`${p}fork\`, and get started...`.squeeze());
+ break;
+ }
+ message.answer(`
+ :warning: ${CONFIG.commands.not_understood}.
+ > \`${CONFIG.commands.prefix}${command}\``.squeeze());
+ break;
+ }
+ }
+ }
+
+ process_generic(message : Message) {
+ const { content } = message;
+ if (content.includes('bot'))
+ message.answer("The hell you sayn' about bots?");
+ // TODO: Process _rules_ appropriately.
+ }
+
+ async last_message(opts) : Promise<string> {
+ const channel = opts.channel as TextChannel;
+
+ if (!opts.offset) opts.offset = 1;
+ if (opts.mention) opts.mentioning = opts.mentioning.trim();
+
+ if (opts.command) {
+ let commands = this._COMMAND_HISTORY
+ .filter(m => m.channel.id === channel.id)
+
+ if (opts.mention) commands = commands.filter(m =>
+ m.author.toString() === opts.mentioning);
+
+ const command = commands.last(opts.offset - 1);
+ if (!command) {
+ channel.send('Cannot expand, no such'
+ + ' command exists in history.');
+ return Promise.reject('COMMAND_NOT_IN_HISTORY');
+ }
+ return Promise.resolve(command.content);
+ }
+
+ let filter = _ => true;
+ if (opts.mention)
+ filter = m => m.author.toString() === opts.mentioning;
+
+ const messages = await channel.fetchMessages({
+ limit: CONFIG.commands.max_history
+ });
+ // Remember that the _latest_ message, is the one that
+ // the user has _just_ sent. This means we ignore the first message.
+ return messages.array()
+ .filter(filter)
+ .get(opts.offset).content;
+ }
+
+ async expand(message : Message) : Promise<string> {
+ // History expansion with !!, !!@, !!^@, !!<ordinal>, etc.
+ const expansions = message.content
+ .replace(/(!!@?\^?\d*)/g, '@EXP$1@EXP')
+ .replace(/(\s)/g, '@EXP$1@EXP')
+ .split('@EXP').squeeze();
+
+ for (let i = 0; i < expansions.length; ++i) {
+ if (expansions[i].startsWith('!!')) {
+ if (expansions[i].length === 2) { // !! expansion
+ expansions[i] = await this.last_message({
+ command: true,
+ channel: message.channel
+ });
+ continue;
+ }
+
+ const [opts, offset] = expansions[i].slice(2)
+ .replace(/(\d+)/, '#$1')
+ .split('#');
+
+ const mention = opts.includes('@');
+ const mentioning = expansions[i + 2];
+ if (mention) expansions[i + 2] = '';
+
+ expansions[i] = await this.last_message({
+ command: !opts.includes('^'),
+ mention: mention,
+ mentioning: mentioning,
+ offset: offset || 1,
+ channel: message.channel
+ });
+ }
+ }
+
+ // TODO: Deal with 'EXPANSION_ERROR's in `expansions`.
+
+ return expansions.join('');
+ }
+
+ @On("message")
+ async on_message(message : Message, client : Client) {
+ console.log('Message acknowledged.');
+ if (SimpOMatic._client.user.id === message.author.id) {
+ return;
+ }
+ console.log('Message received:', message.content);
+
+ const trimmed = message.content.trim();
+ message.content = trimmed;
+ // When finished expanding...
+ this.expand(message).then(content => {
+ if (content.length >= 2000) {
+ message.answer("The expansion for that message was"
+ + " over 2000 characters, what the fuck is wrong with you?");
+ return;
+ }
+ message.content = content;
+ console.log('Expanded message:', message.content);
+
+ if (message.content[0] === CONFIG.commands.prefix) {
+ console.log('Message type: command.')
+ this.process_command(message);
+ } else {
+ console.log('Message type: generic.')
+ this.process_generic(message);
+ }
+ });
+ }
}
const on_termination = () => {
- // Back-up the resultant CONFIG to an external file.
- console.log('Cleaning up...');
- write_file(`${process.cwd()}/export-exit.json`, export_config(CONFIG, {}));
- pastebin_update(export_config(CONFIG, {}));
- // Make sure we saved ok.
- new Promise(res => setTimeout(() => {
- res(null)
- console.log('Clean finished.');
- process.exit(0)
- }, 6000));
+ // Back-up the resultant CONFIG to an external file.
+ console.log('Cleaning up...');
+ write_file(`${process.cwd()}/export-exit.json`, export_config(CONFIG, {}));
+ pastebin_update(export_config(CONFIG, {}));
+ // Make sure we saved ok.
+ new Promise(res => setTimeout(() => {
+ res(null)
+ console.log('Clean finished.');
+ process.exit(0)
+ }, 6000));
};
// Start The Simp'O'Matic.