Simp-O-Matic

Dumb Discord bot in TS.
git clone git://git.knutsen.co/Simp-O-Matic
Log | Files | Refs | README | LICENSE

commit 48d106ac535492f024a044c46fcab1b806d89574
parent 1b9a636f4b951f09d4b2bfd0dd3045b0f59cb5da
Author: Demonstrandum <moi@knutsen.co>
Date:   Tue, 17 Mar 2020 15:51:43 +0000

Switched to Google API, and implemeted a few more commands.

Diffstat:
MHELP.md | 53+++++++++++++++++++++++++++--------------------------
Mgenerate_secrets.sh | 14++++++++++++++
Alib/api/google.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/default.ts | 5+++--
Mlib/main.ts | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mlib/utils.ts | 14++++++++++++++
Mpackage.json | 1+
7 files changed, 224 insertions(+), 71 deletions(-)

diff --git a/HELP.md b/HELP.md @@ -1,11 +1,11 @@ -**KEY:** -How to read the help pages (the notation it uses): +**KEY:** How to read the help pages (the notation it uses): ` ! ` — is the standard command prefix. `[...]` — specifies an option/argument to the command (required). `<...>` — specifies an optional option/argument to the command (not-required). `{a,b}` — represents a choice between a either writing `a` or `b` in its place. ` _ ` — represents the absence of a value at that particular place. +**〈not impl.〉** — the command has not yet been implemented... please send a pull request :pleading_face:. ▬▬▬ @@ -28,15 +28,15 @@ How to read the help pages (the notation it uses): - `!id <who>` — Print ID of user, or self if no-one is specified. - `!get [accessor]` — Get a runtime configuration variable, using JavaScript object dot-notation. - `!set [accessor] [json-value]` — Set a value in the runtime JavaScript configuration object. -- `!uptime` — Display how long the bot has been running for. -- `!clear #[number-of-messages] <@user-name>` — Clear a number of messages, from latest sent in the current channel. Will delete any recent messages, unless a specific username is provided, in which case it will only clear messages sent from that user. +- `!uptime` **〈not impl.〉** — Display how long the bot has been running for. +- `!clear #[number-of-messages] <@user-name>` **〈not impl.〉** — Clear a number of messages, from latest sent in the current channel. Will delete any recent messages, unless a specific username is provided, in which case it will only clear messages sent from that user. - `!alias` — Manage aliases to commands: - - **Have a look at the aliases list, for alternative to long commands!** + - **Have a look at the aliases list, for alternatives to long commands!** - `!alias ![the-alias] ![the-command]` — to add a new alias. - `!alias <ls>` — lists all aliases numerically. - `!alias rm ![the-alias]` — removes the alias by name. - - `!alias rm #[alias-index]` — removes alias by index. -- `!ignore` — What the bot should ignore: + - `!alias rm #[alias-index]` — removes the alias by a numerical index. +- `!ignore` **〈not impl.〉** — What the bot should ignore: - `!ignore channel [#channel-name]` — ignores everything in said channel. - `!ignore user [@user-name]` — ignores everything that user says/does. - `!ignore user speech [@user-name]` — ignores any non-commands given by that user. @@ -47,39 +47,40 @@ How to read the help pages (the notation it uses): - `!ignore whitelist [type] [@name]` — Will exempt certain users or groups from any of the ignore-rules, ever. (`[type]` is either `user` or `group`) - `!ignore <ls>` — lists all ignore rules by type. - `!ignore rm [type] [@name]` — clears all ignore rules for a certain type (types are: `user`, `channel` or `group`). -- `!respond` — How the bot should respond to certain messages: +- `!respond` **〈not impl.〉** — How the bot should respond to certain messages: - `!respond [match] [reply]` — matches an expression said (using regular-expressions, i.e. `/regex/flags`), and replies with a message. - `!respond <ls>` — list all response rules numerically. - `!respond rm #[rule-index]` — removes the response-rule by index. -- `!reject` — Deletes messages meeting certain patterns: +- `!reject` **〈not impl.〉** — Deletes messages meeting certain patterns: - `!reject [match] <reply>` — rejects certain messages, matching a regular-expression (specifying a reply is optional). - `!reject <ls>` — numerically lists all rejection rules. - `!reject rm #[rule-index]` — removes the rejection-rule specified by a numerical index. -- `!replace` — Bots currently do not have the ability to edit other users messages. We can only wait. -- `!cron` — Run commands repeatedly based on some timer (look-up cron syntax for more info): +- `!replace` **〈not impl.〉** — Bots currently do not have the ability to edit other users messages. We can only wait. +- `!cron` **〈not impl.〉** — Run commands repeatedly based on some timer (look-up cron syntax for more info): - `!cron [minute] [hour] [day-of-month] [month] [day-of-week] ![command] <...>` — runs a command (with or without arguments) repeatedly as specified by the schedule signature. - `!cron <ls>` — lists all active cron-jobs numerically. - `!cron rm #[job-index]` — removes a cron-job by index. -- `!choose [comma-separated-values]` — Choose randomly from a list of items, separated by commas. +- `!choose [comma-separated-values]` **〈not impl.〉** — Choose randomly from a list of items, separated by commas. - `!define [word]` — Looks a word up in the Oxford English Dictionary. - `!urban [slang]` — Looks up a piece of slang in the _Urban Dictionary_. - `!search [web-search-terms]` — Performs a web-search and returns the most appropriate URL found. -- `!image [image-search-terms]` — Searches for images specified by the terms given, and send a link to the most relevant one. -- `!gif [gif-search-terms]` — Searches for and returns a GIF matching your search. -- `!cat` — Pussycat pictures... -- `!news [news-search-term]` — Sends you the most relevant news on the specified topic area. -- `!youtube [youtube-search-terms]` — Searches for and returns a relevant _YouTube_ video. -- `!wikipedia` — Search through Wikipedia, returning the most relevant wiki-link. -- `!translate <language> [phrase]` — Translate a phrase from a language (if none specified, it will auto-detect). -- `!wolfram` — Query Wolfram|Alpha. +- `!image [image-search-terms]` — Searches for images specified by the terms given, and sends a link to the most relevant one. +- `!gif [gif-search-terms]` **〈not impl.〉** — Searches for and returns a GIF matching your search. +- `!cat` **〈not impl.〉** — Pussycat pictures... +- `!news [news-search-term]` **〈not impl.〉** — Sends you the most relevant news on the specified topic area. +- `!youtube [youtube-search-terms]` **〈not impl.〉** — Searches for and returns a relevant _YouTube_ video. +- `!wikipedia` **〈not impl.〉** — Search through Wikipedia, returning the most relevant wiki-link. +- `!translate <language> [phrase]` **〈not impl.〉** — Translate a phrase from a language (if none specified, it will auto-detect). +- `!wolfram` **〈not impl.〉** — Query Wolfram|Alpha. - `!say [phrase]` — Repeats what you told it to say. - `!milkies` — In case you're feeling thirsty... -- `!cowsay <options> [phrase]` — Make a cow say something, using Unix-like command line arguments. -- `!figlet <options> [phrase]` — Print text in ASCII format, using Unix-like command line arguments. -- `!roll <upper-bound>` — Roll a dice, default upper bound is 6. -- `!summon [@user-name]` — Summon someone to the server by making the bot poke them in their DMs about it. -- `!mock [phrase]` — Say something, _bUt iN a MocKiNg WaY_... -- `!boomer [phrase]` — Say something, but in the way your demented boomer uncle would write it on Facebook. +- `!cowsay <options> [phrase]` **〈not impl.〉** — Make a cow say something, using Unix-like command line arguments. +- `!figlet <options> [phrase]` **〈not impl.〉** — Print text in ASCII format, using Unix-like command line arguments. +- `!roll <upper-bound>` **〈not impl.〉** — Roll a dice, default upper bound is 6. +- `!8ball` **〈not impl.〉** — Ask a question, receive a response. +- `!summon [@user-name]` **〈not impl.〉** — Summon someone to the server by making the bot poke them in their DMs about it. +- `!mock [phrase]` **〈not impl.〉** — Say something, _bUt iN a MocKiNg WaY_... +- `!boomer [phrase]` **〈not impl.〉** — Say something, but in the way your demented boomer uncle would write it on Facebook. ▬▬▬ diff --git a/generate_secrets.sh b/generate_secrets.sh @@ -20,6 +20,20 @@ cat <<- JSON "oxford": { "id": "$OXFORD_ID", "key": "$OXFORD_KEY" + }, + "google": { + "api_key": "$GOOGLE_API_KEY", + "search_id": "$GOOGLE_SEARCH_ID", + "type": "$GOOGLE_TYPE", + "project_id": "$GOOGLE_PROJECT_ID", + "private_key_id": "$GOOGLE_PRIVATE_KEY_ID", + "private_key": "$GOOGLE_PRIVATE_KEY", + "client_email": "$GOOGLE_CLIENT_EMAIL", + "client_id": "$GOOGLE_CLIENT_ID", + "auth_uri": "$GOOGLE_AUTH_URI", + "token_uri": "$GOOGLE_TOKEN_URI", + "auth_provider_x509_cert_url": "$GOOGLE_AUTH_PROVIDER_X509_CERT_URL", + "client_x509_cert_url": "$GOOGLE_CLIENT_X509_CERT_URL" } } JSON diff --git a/lib/api/google.ts b/lib/api/google.ts @@ -0,0 +1,58 @@ +import { google } from 'googleapis'; + +type CSE = { + kind: 'image' | 'web', + key: string, + id: string, + query: string +}; + +// Cache grows from the bottom, and deletes from the top. +// i.e. old cached items get deleted, since they're unpopular. +const CACHE_SIZE = 40; +const CACHE = { + 'aaron obese': "https://assets3.thrillist.com/v1/image/2765017/size/tmg-article_tall;jpeg_quality=20.jpg\n>>> Aaron obese xD, shut up instgen...", + 'druggie': "https://vignette.wikia.nocookie.net/sausage-party-recipe-book/images/e/e7/Druggie.png/revision/latest/top-crop/width/360/height/450?cb=20170212165040\n>>> Hurr durr, arron's a shroomer lmao.", + 'aaron fish': "https://i.pinimg.com/280x280_RS/fa/b5/96/fab5962b97d464781f65952b6b63e4a0.jpg\n>>> arron fish? arron fish. Blub blub." +}; + +// TODO: Reject results if they're from: +// Urban Dictionary, +// YouTube or Wikipedia. +// These web-places already have commands given to them. + +const web_search = (param : CSE) => new Promise((resolve, reject) => { + const cache_keys = Object.keys(CACHE); + // Retrieve cached query. + if (param.query in CACHE) { + return resolve(`${CACHE[param.query]} (cached response)`) + } else if (cache_keys.length > CACHE_SIZE) { + // Delete a few, so we can delete less frequently. + delete CACHE[cache_keys[0]]; + delete CACHE[cache_keys[1]]; + delete CACHE[cache_keys[2]]; + } + + const cs = google.customsearch('v1'); + + cs.cse.list({ + auth: param.key, + cx: param.id, + q: param.query, + searchType: (param.kind === 'web') ? undefined : param.kind, + start: 0, + num: 1 + }).then(res => { + if (!res.data || !res.data.items || res.data.items.length === 0) + return reject('No such results found.') + + const item = res.data.items[0]; + const answer = `${item.link}\n>>> ${item.title}`; + // Cache this query + CACHE[param.query] = answer; + return resolve(answer); + }).catch(e => + reject(`No results, or API capped...\n\`\`\`\n${e}\n\`\`\``)); +}); + +export default web_search; diff --git a/lib/default.ts b/lib/default.ts @@ -20,6 +20,7 @@ export default { 'yt': 'youtube', 'y': 'youtube', 'd': 'define', + 'def': 'define', 'oed': 'define', 'oxford': 'define', 'ud': 'urban', @@ -80,8 +81,8 @@ export default { // For real this time: // -> `accelarion#0764` // a.k.a. instcel, - // a.k.a. instgen - // a.k.a. installgentoo + // a.k.a. instgen, + // a.k.a. installgentoo. "409461942495871016": { commands: true, commands_elevated: false, diff --git a/lib/main.ts b/lib/main.ts @@ -15,14 +15,14 @@ import { execSync as shell } from 'child_process'; // Local misc/utility functions. import './extensions'; import { deep_merge, pp, compile_match, - export_config, access } 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. import DEFAULT_CONFIG from './default'; // API specific modules. -import web_search from './api/contextual'; +import web_search from './api/google'; import oed_lookup from './api/oxford'; import urban_search from './api/urban'; import { Channel } from 'discord.js'; @@ -56,29 +56,21 @@ const HELP_SECTIONS = HELP.toString() .split('@@@') .filter(e => !!e && !!e.trim()); -let acc = ""; -let new_messages : string[] = []; // This assumes no two help-entries would ever // be greater than 2000 characters long -for (const msg of HELP_SECTIONS) - if (acc.length + msg.length >= 2000) { - new_messages.push(acc); - acc = msg; - } else { acc += msg; } -new_messages.push(acc); - -const HELP_MESSAGES = new_messages; - -const ALL_HELP = [ +const HELP_MESSAGES = glue_strings(HELP_SECTIONS); +const ALL_HELP = glue_strings([ HELP_KEY, '\n▬▬▬\n', ...HELP_MESSAGES, '\n▬▬▬\n', HELP_SOURCE -]; +]); const KNOWN_COMMANDS = HELP_SECTIONS.map(e => e.slice(5).replace(/(\s.*)|(`.*)/g, '')); +const GIT_URL = 'https://github.com/Demonstrandum/Simp-O-Matic'; + // Log where __dirname and cwd are for deployment. console.log('File/Execution locations:', { '__dirname': __dirname, @@ -114,7 +106,7 @@ export class SimpOMatic { const words = content.tail().split(' '); const args = words.tail(); - let command = words[0]; + let command = words[0].toLowerCase(); if (CONFIG.commands.aliases.hasOwnProperty(command)) command = CONFIG.commands.aliases[command].trim().squeeze(); @@ -165,13 +157,24 @@ export class SimpOMatic { message.answer(`No such command/help-page (\`${command}\`).`); else message.answer(`**Help (\`${command}\`):**\n` - + HELP_SECTIONS[help_index]); + + HELP_SECTIONS[help_index].trim()); break; } case 'id': { - const reply = `User ID: ${message.author.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(); + Message ID: \`${message.id}\``.squeeze(); console.log(`Replied: ${reply}`); message.answer(reply); break; @@ -217,6 +220,41 @@ export class SimpOMatic { 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: @@ -234,6 +272,20 @@ export class SimpOMatic { 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': { @@ -250,38 +302,33 @@ export class SimpOMatic { } message.answer(`Current command prefix is: \`${CONFIG.commands.prefix}\`.`); break; + } case 'ignore': { + // Man alive, someone else do this please. + break; + } case 'response': { + + break } case 'search': { - const query = args.join(' '); + const query = args.join(' ').toLowerCase(); web_search({ - type: 'web', + kind: 'web', query, - key: SECRETS.rapid.key - }).then((res: object) => { - if (res['value'].length === 0) { - message.answer('No such results found.'); - return; - } - message.answer(`Web search for ‘${query}’, \ - found: ${res['value'][0].url}`.squeeze()); - }).catch(e => message.answer(`Error fetching results:\n${e}`)); + 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(' '); + const query = args.join(' ').toLowerCase(); web_search({ - type: 'image', + kind: 'image', query, - key: SECRETS.rapid.key - }).then(res => { - if (res['value'].length === 0) { - message.answer('No such images found.'); - return; - } - message.answer(`Image found for ‘${query}’: \ - ${res['value'][0].url}`.squeeze()); - }).catch(e => - message.answer(`Error fetching image:\n${e}`)); + key: SECRETS.google.api_key, + id: SECRETS.google.search_id + }).then((res) => message.answer(res)) + .catch(e => message.answer(e)); break; } case 'define': { message.answer('Looking in the Oxford English Dictionary...'); @@ -366,7 +413,7 @@ export class SimpOMatic { } case 'ily': { message.answer('Y-you too...'); break; - }case 'say': {2 + } case 'say': { message.answer(`Me-sa says: “${args.join(' ')}”`); break; } case 'export': { @@ -395,10 +442,27 @@ export class SimpOMatic { message.answer(`A copy of this export (\`export-${today}.json\`) \ has been saved to the local file system.`.squeeze()); 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()); diff --git a/lib/utils.ts b/lib/utils.ts @@ -2,6 +2,20 @@ import { inspect } from 'util'; import deep_clone from 'deepcopy'; import './extensions'; +// This assumes no two string-array entries +// would ever be greater than 2000 characters long. +export const glue_strings = arr => { + let acc = ""; + const new_strings = []; + for (const msg of arr) + if (acc.length + msg.length >= 2000) { + new_strings.push(acc); + acc = msg; + } else { acc += msg; } + new_strings.push(acc); + return new_strings; +}; + export const access = (obj: any, shiftable: string[]) => (shiftable.length === 0) ? obj diff --git a/package.json b/package.json @@ -36,6 +36,7 @@ "@types/ws": "^7.2.2", "deepcopy": "^2.0.0", "discord.js": "11.6.1", + "googleapis": "^48.0.0", "tslib": "^1.11.1", "typescript": "^3.8.3", "unirest": "^0.6.0"