commit 9988c4ad1258af32328b6421b78794d5c29fcb35
parent e2536d0ab96905e9271c66e5777eb0929378474d
Author: Demonstrandum <moi@knutsen.co>
Date:   Sun,  8 Mar 2020 15:16:22 +0000
Added !help and some more commands.
Diffstat:
17 files changed, 656 insertions(+), 1101 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -2,8 +2,18 @@
 secrets.json
 export_secrets.sh
 
-# Node
+# Node / NPM / Yarn
 node_modules/
+package-lock.json
+yarn.lock
 
-# Build dir
+# Build files
 build/
+export.json
+export*.json
+
+# Deployment info
+.now
+
+# Logs
+*.log
diff --git a/HELP.md b/HELP.md
@@ -0,0 +1,45 @@
+**KEY:**
+`  !  ` — is the standard command prefix.
+`[...]` — specifies an option/argument to the command (required).
+`<...>` — specifies an optional option/argument to the command (not-required).
+
+▬▬▬
+
+- `!help` — Shows this page.
+- `!export` — Exports current configuration, and saves it.
+- `!prefix [new]` — Changes the prefix for sending this bot commands (default is `!`). Can only be one (1) character/symbol/grapheme/rune long.
+- `!id <who>` — Print ID of user, or self if no-one is specified.
+- `!alias` — Manage aliases to 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:
+  - `!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.
+  - `!ignore user commands [@user-name]` — ignores all commands that user tries to use.
+  - `!ignore user commands elevated [@user-name]` — ignores all elevated/high-permissions commands that user tries to use.
+  - `!ignore group ...` — works exactly like ignore-user, but for groups instead.
+  - `!ignore not ...` — works exactly like all other ignore-commands, but does the opposite (toggles that rule ignore-rule on-off).
+  - `!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 [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 [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.
+- `!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.
+- `!youtube [youtube-search-terms]` — Searches for and returns a relevant _YouTube_ video.
+- `!cron` — Run commands repeatedly based on some timer (Google 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.
diff --git a/README.md b/README.md
@@ -0,0 +1,71 @@
+# The Simp'O'Matic
+
+## WIP
+
+Currently most of the features in the `HELP.md` page, have not been
+implemented.  Please my fren, if you have the time, send some pull
+requests my way.
+
+Yours, Sammy (in desperation for a nice FOSS-bot).
+
+## Help / Bot-Commands
+
+See [HELP.md](./HELP.md) file (keep in mind, that the file has
+been formatted for better viewing through Discord, might look funky
+with GitHub rendering).
+
+## Getting Up & Running
+
+Make sure you have `node` (`v12.x`) and `yarn`installed
+(`npm` also possible).
+
+- Clean up from previous build/install:
+```sh
+yarn clean
+```
+- Install dependencies:
+```sh
+yarn install
+```
+- Build/Compile files:
+```sh
+yarn build
+```
+- Run the bot locally:
+```sh
+yarn run
+```
+- Or deploy it with `now`:
+```sh
+yarn deploy
+```
+
+In both cases (`deploy` or `start`) you'll need your secrets set up
+(API keys etc.).
+
+### Local Secrets
+
+Make sure locally, you have the following secrets exported
+as environment variables:
+```sh
+export BOT_API_TOKEN="exampleExampleExampleExample"
+export RAPID_API_KEY="exampleExampleExampleExample"
+export CLIENT_KEY="exampleExampleExampleExample"
+export CLIENT_ID="exampleExampleExampleExample"
+export OXFORD_ID="exampleExampleExampleExample"
+export OXFORD_KEY="exampleExampleExampleExample"
+```
+
+### Now Deployment Secrets
+
+`now` has a `secrets` functionality, which will store your secrets, and
+export them as environment variables, for your `now`, you can do:
+```sh
+now secrets add discord-bot-api-token "$BOT_API_TOKEN"
+now secrets add rapid-api-key "$RAPID_API_KEY"
+now secrets add discord-client-key "$CLIENT_KEY"
+now secrets add discord-client-id "$CLIENT_ID"
+now secrets add oxford-dictionary-id "$OXFORD_ID"
+now secrets add oxford-dictionary-key "$OXFORD_KEY"
+```
+For some context, have a look in `./now.json`.
diff --git a/bot.json b/bot.json
@@ -5,5 +5,20 @@
 
     "commands": {
         "prefix": "!"
+    },
+
+    "rules": {
+        "respond": [
+            {
+                "match": "/Good (Morning|Day) (Star|Sun)shine/i",
+                "response": "The Earth says Hello!"
+            }
+        ],
+        "replace": [
+            {
+                "match": "/Yahweh/i",
+                "response": "Adonai"
+            }
+        ]
     }
 }
diff --git a/build.sh b/build.sh
@@ -1,11 +1,17 @@
 #!/bin/sh
 
-[ ! -d "./node_modules" ] && echo "Installing..." && npm install
+bold="$(tput bold)"
+reset="$(tput sgr0)"
+
+[ ! -d "./node_modules" ] && echo "${bold}Installing...${reset}" && yarn install
 rm -rf ./build
-mkdir -p ./build
+mkdir -p ./build ./public
 
-echo "Copying files..."
-cp ./bot.json ./generate_secrets.sh ./build
+echo "${bold}Copying config files...${reset}"
+cp ./bot.json ./generate_secrets.sh ./HELP.md ./build
 
+echo "${bold}Compiling TypeScript...${reset}"
+./node_modules/.bin/tsc -b ./tsconfig.json
 
 
+echo -e "\n${bold}Build done.${reset}"
diff --git a/generate_secrets.sh b/generate_secrets.sh
@@ -14,8 +14,12 @@ cat <<- JSON
         "id": "$CLIENT_ID"
     },
 
-    "contextual": {
-        "key": "$CONTEXTUAL_API_KEY"
+    "rapid": {
+        "key": "$RAPID_API_KEY"
+    },
+    "oxford": {
+        "id": "$OXFORD_ID",
+        "key": "$OXFORD_KEY"
     }
 }
 JSON
diff --git a/lib/api/urban.ts b/lib/api/urban.ts
@@ -0,0 +1,25 @@
+import unirest from 'unirest';
+
+export const urban_search = options => new Promise((resolve, reject) => {
+    console.log('Searching Urban Dictionary, with options: ', options);
+
+    const url = 'https://mashape-community-urban-dictionary.p.rapidapi.com/define';
+
+    const req = unirest('GET', url);
+
+    req.query({
+        "term": options.query
+    });
+
+    req.headers({
+        "x-rapidapi-host": "mashape-community-urban-dictionary.p.rapidapi.com",
+        "x-rapidapi-key": options.key
+    });
+
+    req.end(res => {
+        if (res.error) return reject(res.error);
+        return resolve(res.body);
+    });
+});
+
+export default urban_search;
diff --git a/lib/api/web.ts b/lib/api/web.ts
@@ -0,0 +1,41 @@
+import unirest from 'unirest';
+
+export const web_search = options => new Promise((resolve, reject) => {
+    console.log('Searching the web, with options: ', options);
+
+    let api = 'WebSearchAPI';
+    switch (options.type) {
+        case 'image':
+            api = 'ImageSearchAPI';
+            break;
+        case 'web':
+            api = 'WebSearchAPI';
+            break;
+        case 'news':
+            api = 'NewsSearchAPI';
+            break;
+    }
+    const url = `https://contextualwebsearch-websearch-v1.p.rapidapi.com/api/Search/${api}`;
+
+    const req = unirest('GET', url);
+
+    req.query({
+        "autoCorrect": "false",
+        "pageNumber": "1",
+        "pageSize": "10",
+        "q": options.query,
+        "safeSearch": "false"
+    });
+
+    req.headers({
+        "x-rapidapi-host": "contextualwebsearch-websearch-v1.p.rapidapi.com",
+        "x-rapidapi-key": options.key
+    });
+
+    req.end(res => {
+        if (res.error) return reject(res.error);
+        return resolve(res.body);
+    });
+});
+
+export default web_search;
diff --git a/lib/default.ts b/lib/default.ts
@@ -0,0 +1,102 @@
+/// A default config file, that adds some basic functionality,
+///  and to act as a reference to how the config shall be
+///  laid out.  All fields are accounted for here.
+
+export default {
+    name: "Simp'O'Matic",
+    tag: "#1634",
+    permissions: 8,
+
+    commands: {
+        prefix: '!',
+        not_understood: "Command not understood",
+        aliases: {
+            'img': 'image',
+            'i': 'image',
+            'h': 'help',
+            's': 'search',
+            'yt': 'youtube',
+            'd': 'define',
+            'oed': 'define',
+            'ud': 'urban',
+            'blacklist': 'ignore',
+            'whitelist': 'ignore whitelist',
+            'w': 'weather',
+            'reply': 'respond',
+            'reject': 'delete'
+        },
+    },
+
+    rules: {
+        // Below are the different kinds of _rules_.
+        respond: [
+            {
+                match: "/^\\s*thanks.*\\s*$/i",
+                response: 'Obama.'
+            },
+        ],
+        reject: [
+            {
+                match: "/\\.{4,}/",
+                response: null
+            },
+        ],
+        replace: [  // Message editing functionality not a thing yet...
+            {
+                match: "/tbh/i",
+                response: 'desu'
+            },
+        ],
+        // Blacklist (initially everyone can do everything,
+        //  except for those listed specifically on this list).
+        blacklist: {
+            channels: [
+                'music', 'news'
+            ],
+            users: {
+                // Should all be numbers/hashes, this one is bogus:
+                "instcel": {
+                    commands: true,
+                    commands_elevated: false,
+                    speech: true,
+                },
+                // For real this time:
+                // -> `accelarion#0764`
+                //     a.k.a. instcel,
+                //     a.k.a. instgen
+                //     a.k.a. installgentoo
+                "409461942495871016": {
+                    commands: true,
+                    commands_elevated: false,
+                    speech: true
+                }
+            },
+            groups: {
+                // Should all be numbers/hashes, these are bogus.
+                "obese": {
+                    commands_elevated: false,
+                },
+                "bpd": {
+                    commands: false,
+                }
+            },
+        },
+
+        // In case you blacklist @everyone or something,
+        // you can override completely, to obtain all permissions just
+        // by putting them on the whitelist.
+        whitelist: {
+            users: [
+                // Dr. Henry Kissinger#5457
+                "265958795254038535",
+                // Danny#1986
+                "541761315887120399"
+            ],
+            groups: [
+                // Will all be numbers/hashes too.
+                "bourgeoisie"
+            ]
+        }
+    }
+
+}
diff --git a/lib/extensions.ts b/lib/extensions.ts
@@ -0,0 +1,44 @@
+// Array Extensions:
+interface Array<T> {
+    unique() : Array<T>
+    mut_unique(): Array<T>
+    mut_map(f : (T) => any) : Array<any>
+}
+
+Array.prototype.unique = function () {
+    return this.filter((e, i) => this.indexOf(e) === i)
+}
+
+Array.prototype.mut_unique = function () {
+    const uniq = this.unique();
+    Object.assign(this, uniq);
+    this.splice(uniq.length);
+    return this;
+}
+
+Array.prototype.mut_map = function (f) {
+    for (const i in this) {
+        this[i] = f(this[i]);
+    }
+    return this;
+}
+
+// String Extensions:
+interface String {
+    squeeze() : string
+}
+
+String.prototype.squeeze = function () {
+    return this.split(/[ ]+/).join(' ');
+};
+
+
+// Number Extensions:
+interface Number {
+    round_to(dp : number) : number
+}
+
+Number.prototype.round_to = function (dp : number) {
+    const exp = 10 ** dp;
+    return Math.round(this.valueOf() * exp) / exp;
+};
diff --git a/lib/main.ts b/lib/main.ts
@@ -1,28 +1,62 @@
 import { Discord, On, Client } from '@typeit/discord';
-import { Message } from 'discord.js';
+import { Message, MessageAttachment } from 'discord.js';
 
-import { readFileSync as read } from 'fs';
-import { execSync } from 'child_process';
+import {
+    readFileSync  as  read_file,
+    writeFileSync as write_file,
+} from 'fs';
+import { execSync as shell } from 'child_process';
+import { inspect } from 'util';
 
-import './utils';
-import { search } from './search_api';
+import './extensions';
+import { deep_merge, pp, compile_match, export_config } from './utils';
+import DEFAULT_CONFIG from './default';
+import web_search from './api/web';
+import urban_search from './api/urban';
 
-const BOT_CONFIG = JSON.parse(read('./bot.json', 'utf-8'));
-const SECRETS = JSON.parse(execSync('sh ./generate_secrets.sh').toString());
+// 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')));
+
+// Precompile all regular-expressions in known places.
+['respond', 'reject', 'replace']
+    .forEach(name => CONFIG.rules[name].mut_map(compile_match))
+
+// Store secrets in an object, retrieved from shell's
+//  environment variables.
+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 = read_file('./HELP.md');
+const help_sections = HELP.toString()
+    .replace(/\n  -/g, '\n      \u25b8')
+    .replace(/\n- /g, '@@@\n\u2b25 ')
+    .split('@@@');
+
+let acc = "";
+let new_messages = [];
+
+for (const msg of help_sections)
+    if (acc.length + msg.length >= 1990) {
+        new_messages.push(acc);
+        acc = "";
+    } else { acc += msg; }
+
+new_messages.push(acc);
+const HELP_MESSAGES = new_messages;
+
+console.log(HELP_MESSAGES);
 
 @Discord
 export class SimpOMatic {
     private static _client : Client;
-    private _prefix : string = BOT_CONFIG.commands.prefix || '!';
-    private _not_understood : string = BOT_CONFIG.not_understood
-        || "Command not understood";
 
     constructor() {
-        console.log('Configured Variables: ', {
-            "_prefix": this._prefix,
-            "_not_understood": this._not_understood
-        });
-        console.log('Secrets: ', SECRETS);
+        console.log('Secrets:', pp(SECRETS));
+        console.log('Configured Variables:', pp(CONFIG));
     }
 
     static start() {
@@ -33,50 +67,153 @@ export class SimpOMatic {
         );
     }
 
-    @On("message")
-    async on_message(message : Message, client : Client) {
-        console.log('Message acknowledged.');
-        if (SimpOMatic._client.user.id === message.author.id
-        || message.content[0] !== this._prefix) {
-            return;
-        }
+    process_command(message : Message) {
+        const words = message.content.slice(1).split(' ');
 
-        console.log('Message received: ', message.content);
+        let command = words[0];
+        if (CONFIG.commands.aliases.hasOwnProperty(command))
+            command = CONFIG.commands.aliases[command];
+        command = command.toLowerCase();
 
-        const words = message.content.slice(1).toLowerCase().split(' ');
-        const command = words[0];
         const args = words.slice(1);
 
+        console.log('Received command: ', [command, args]);
+
         switch (command) {
-            case "ping":
-                message.reply("ponggers.");
+            case "ping": {
+                message.reply("PONGGERS!");
+                break;
+            } case 'help': {
+                message.reply('**HELP:**');
+                for (const msg of HELP_MESSAGES)
+                    message.channel.send(msg);
+                break;
+            } case "id": {
+                const reply = `User ID: ${message.author.id}
+                    Author: ${message.author}
+                    Message ID: ${message.id}`.squeeze();
+                console.log(`Replied: ${reply}`);
+                message.reply(reply);
+                break;
+            } case "search": {
+                const query = args.join(' ');
+
+                web_search({
+                    type: "search",
+                    query,
+                    key: SECRETS.rapid.key
+                }).then((res: object) => {
+                    if (res['value'].length === 0) {
+                        message.reply('No such results found.');
+                        return;
+                    }
+                    message.reply(`Web search for ‘${query}’, found: ${res['value'][0].url}`);
+                }).catch(e => message.reply(`Error fetching results:\n${e}`));
                 break;
-            case "img":
+            } case "image": {
                 const query = args.join(' ');
-                search({
+
+                web_search({
                     type: "image",
                     query,
-                    key: SECRETS.contextual.key
-                }).then((res : object) => {
-                    if (res['value'].length === 0)
+                    key: SECRETS.rapid.key
+                }).then(res => {
+                    if (res['value'].length === 0) {
                         message.reply('No such images found.');
-                    message.reply(`Image of ${query}: ${res['value'][0].url}`);
-                }).catch(err => message.reply('Error fetching image.'));
+                        return;
+                    }
+                    message.reply(`Image found for ‘${query}’: ${res['value'][0].url}`);
+                }).catch(e =>
+                    message.reply(`Error fetching image:\n${e}`));
                 break;
-            case "milkies":
-                message.reply(`${(4 + Math.random() * 15).round_to(3)} gallons of milkies \
-                    have been deposited in your mouth.`.squeeze());
+            } case "urban": {
+                const query = args.join(' ');
+                urban_search({ query, key: SECRETS.rapid.key }).then(res => {
+                    if (res['list'].length === 0) {
+                        message.reply(`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.reply(`Urban Dictionary defines \
+                        ‘${query}’, as:\n> ${def}`.squeeze());
+                    message.channel.send(`Link: ${entry.permalink}`);
+                }).catch(e => message.reply(`Error fetching definition:\n${e}`));
+                break;
+            } case "milkies": {
+                message.reply(`${(4 + Math.random() * 15).round_to(3)} gallons \
+                    of milkies have been deposited in your mouth.`.squeeze());
+                break;
+            } case "say": {2
+                message.reply(`Me-sa says: “${args.join(' ')}”`);
                 break;
-            case "say":
-                message.reply(`Me says: "${args.join(' ')}"`);
+            } 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, {}));
+
+                if (export_string.length < 1980) {
+                    message.channel.send("```json\n" + export_string + "\n```");
+                } else {
+                    const attach = new MessageAttachment(file_dest);
+                    attach.name = file_name;
+                    message.channel.send("**Export:**", attach);
+                }
+
+                message.reply(`A copy of this export (\`export-${today}.json\`) \
+                    has been saved to the local file system.`.squeeze());
                 break;
-            default:
-                message.reply(
-                    `\`[!!]\` ${this._not_understood}. (\`${command}\`)`);
+            }
+            default: {
+                message.reply(`
+                    :warning: ${CONFIG.commands.not_understood}.
+                    > \`${CONFIG.commands.prefix}${command}\``.squeeze());
                 break;
+            }
         }
     }
-}
 
+    process_generic(message : Message) {
+        const { content } = message;
+        if (content.includes('bot'))
+            message.reply("The hell you sayn' about bots?");
+        // TODO: Process _rules_ appropriately.
+    }
+
+    @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);
 
+        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);
+        }
+    }
+}
+
+// Start The Simp'O'Matic.
 SimpOMatic.start();
+
+// Back-up the resultant CONFIG to an external file.
+write_file(`${process.cwd()}/export.json`, export_config(CONFIG, {}));
diff --git a/lib/search_api.ts b/lib/search_api.ts
@@ -1,39 +0,0 @@
-import unirest from 'unirest';
-
-export const search = options => new Promise((resolve, reject) => {
-    console.log('Searching the web, with options: ', options);
-
-    let api = 'WebSearchAPI';
-    switch (options.type) {
-        case 'image':
-            api = 'ImageSearchAPI';
-            break;
-        case 'web':
-            api = 'WebSearchAPI';
-            break;
-        case 'news':
-            api = 'NewsSearchAPI';
-            break;
-    }
-    const url = `https://contextualwebsearch-websearch-v1.p.rapidapi.com/api/Search/${api}`;
-
-    const req = unirest('GET', url);
-
-    req.query({
-        "autoCorrect": "false",
-        "pageNumber": "1",
-        "pageSize": "10",
-        "q": options.query,
-        "safeSearch": "false"
-    });
-
-    req.headers({
-        "x-rapidapi-host": "contextualwebsearch-websearch-v1.p.rapidapi.com",
-        "x-rapidapi-key": options.key
-    });
-
-    req.end(res => {
-        if (res.error) return reject(res.error);
-        return resolve(res.body);
-    });
-});
diff --git a/lib/utils.ts b/lib/utils.ts
@@ -1,20 +1,86 @@
-// String Utils:
+import { inspect } from 'util';
+import deep_clone from 'deepcopy';
+import './extensions';
 
-String.prototype.squeeze = function () {
-    return this.split(/\s+/).join(' ');
-};
+export const type: (obj: any) => string = (global => obj =>
+    (obj === global)
+        ? 'global'
+        : ({})
+            .toString.call(obj)
+            .match(/\s([a-z|A-Z]+)/)[1]
+            .toLowerCase())(this);
 
-interface String {
-    squeeze() : string
-}
+export const pp = o => inspect(o, {
+    colors: true,
+    showHidden: false,
+    depth: 8
+});
+
+export const deep_merge_pair = (target, source) => {
+    Object.keys(source).forEach(key => {
+        const target_value = target[key];
+        const source_value = source[key];
 
+        if (Array.isArray(target_value)
+         && Array.isArray(source_value)) {
+            target[key] = target_value.concat(...source_value);
+        }
+        else if (type(target_value) === 'object'
+              && type(source_value) === 'object') {
+            target[key] = deep_merge_pair(target_value, source_value);
+        }
+        else {
+            target[key] = source_value;
+        }
+    });
 
-// Number Utils:
-Number.prototype.round_to = function (dp : number) {
-    const exp = 10 ** dp;
-    return Math.round(this.valueOf() * exp) / exp;
+    return target;
 }
 
-interface Number {
-    round_to(dp : number) : number
+export const deep_merge = (...objects) =>
+    (objects.length === 2)
+        ? deep_merge_pair(objects[0], objects[1])
+        : deep_merge_pair(objects[0], deep_merge(objects.slice(1)));
+
+
+export const parse_regex = (s : string) => {
+    let temp = s.split('/');
+    const options = temp.pop();
+    temp.shift();
+    const contents = temp.join('/');
+    return new RegExp(contents, options);
+};
+
+export const compile_match = obj => {
+    const o = deep_clone(obj);
+    if (type(o.match) === 'string') {
+        o.match = parse_regex(o.match);
+    }
+    return o;
+};
+
+const recursive_regex_to_string = o => {
+    if (type(o) === 'regexp') {
+        return o.toString();
+    }
+    if (type(o) === 'object' || type(o) === 'array') {
+        for (const key in o) {
+            o[key] = recursive_regex_to_string(o[key]);
+        }
+        return o;
+    }
+    return o;
 }
+
+export const export_config = (obj, { ugly = false }) => {
+    const o = recursive_regex_to_string(deep_clone(obj));
+    // Make sure all rules are unique,
+    //  i.e. eliminate duplicate rules.
+    ['respond', 'reject', 'replace']
+        .forEach(name => o.rules[name] = o.rules[name]
+            .map(JSON.stringify)
+            .unique()
+            .map(JSON.parse));
+
+    return JSON.stringify(o, null, ugly ? null : 4);
+};
diff --git a/now.json b/now.json
@@ -2,10 +2,13 @@
     "name": "simpomatic",
     "version": 1,
     "public": true,
+
     "env": {
         "BOT_API_TOKEN": "@discord-bot-api-token",
-        "CONTEXTUAL_API_KEY": "@contextual-web-api-key",
+        "RAPID_API_KEY": "@rapid-api-key",
         "CLIENT_KEY": "@discord-client-key",
-        "CLIENT_ID": "@discord-client-id"
+        "CLIENT_ID": "@discord-client-id",
+        "OXFORD_ID": "@oxford-dictionary-id",
+        "OXFORD_KEY": "@oxford-dictionary-key"
     }
 }
diff --git a/package-lock.json b/package-lock.json
@@ -1,987 +0,0 @@
-{
-    "name": "simpomatic",
-    "version": "0.1.0",
-    "lockfileVersion": 1,
-    "requires": true,
-    "dependencies": {
-        "@discordjs/collection": {
-            "version": "0.1.5",
-            "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.5.tgz",
-            "integrity": "sha512-CU1q0UXQUpFNzNB7gufgoisDHP7n+T3tkqTsp3MNUkVJ5+hS3BCvME8uCXAUFlz+6T2FbTCu75A+yQ7HMKqRKw=="
-        },
-        "@typeit/discord": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/@typeit/discord/-/discord-1.0.3.tgz",
-            "integrity": "sha512-s0ahyW51Rz3jDZNODbK5kJoZxBwxahQ5CevpaimZ5KlJylSBA8lM39IRx5QlBOG9TMVktJ2KehMzUDqxkm41PQ==",
-            "requires": {
-                "glob": "^7.1.4"
-            }
-        },
-        "@types/node": {
-            "version": "13.7.7",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.7.tgz",
-            "integrity": "sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg=="
-        },
-        "@types/ws": {
-            "version": "7.2.2",
-            "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.2.tgz",
-            "integrity": "sha512-oqnI3DbGCVI9zJ/WHdFo3CUE8jQ8CVQDUIKaDtlTcNeT4zs6UCg9Gvk5QrFx2QPkRszpM6yc8o0p4aGjCsTi+w==",
-            "requires": {
-                "@types/node": "*"
-            }
-        },
-        "abab": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",
-            "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg=="
-        },
-        "abort-controller": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
-            "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
-            "requires": {
-                "event-target-shim": "^5.0.0"
-            }
-        },
-        "acorn": {
-            "version": "7.1.1",
-            "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
-            "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg=="
-        },
-        "acorn-globals": {
-            "version": "4.3.4",
-            "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz",
-            "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==",
-            "requires": {
-                "acorn": "^6.0.1",
-                "acorn-walk": "^6.0.1"
-            },
-            "dependencies": {
-                "acorn": {
-                    "version": "6.4.0",
-                    "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
-                    "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
-                }
-            }
-        },
-        "acorn-walk": {
-            "version": "6.2.0",
-            "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
-            "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
-        },
-        "ajv": {
-            "version": "6.12.0",
-            "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
-            "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
-            "requires": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            }
-        },
-        "asn1": {
-            "version": "0.2.4",
-            "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
-            "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
-            "requires": {
-                "safer-buffer": "~2.1.0"
-            }
-        },
-        "assert-plus": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-            "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
-        },
-        "async": {
-            "version": "0.9.2",
-            "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
-            "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
-        },
-        "asynckit": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-            "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
-        },
-        "aws-sign2": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-            "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
-        },
-        "aws4": {
-            "version": "1.9.1",
-            "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
-            "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
-        },
-        "axios": {
-            "version": "0.18.1",
-            "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
-            "integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
-            "requires": {
-                "follow-redirects": "1.5.10",
-                "is-buffer": "^2.0.2"
-            }
-        },
-        "balanced-match": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-            "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
-        },
-        "bcrypt-pbkdf": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-            "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
-            "requires": {
-                "tweetnacl": "^0.14.3"
-            },
-            "dependencies": {
-                "tweetnacl": {
-                    "version": "0.14.5",
-                    "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-                    "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
-                }
-            }
-        },
-        "brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "requires": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "browser-process-hrtime": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
-            "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw=="
-        },
-        "caseless": {
-            "version": "0.12.0",
-            "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-            "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
-        },
-        "combined-stream": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-            "requires": {
-                "delayed-stream": "~1.0.0"
-            }
-        },
-        "concat-map": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
-        },
-        "core-util-is": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-            "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-        },
-        "cssom": {
-            "version": "0.4.4",
-            "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
-            "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw=="
-        },
-        "cssstyle": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.2.0.tgz",
-            "integrity": "sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==",
-            "requires": {
-                "cssom": "~0.3.6"
-            },
-            "dependencies": {
-                "cssom": {
-                    "version": "0.3.8",
-                    "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
-                    "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
-                }
-            }
-        },
-        "dashdash": {
-            "version": "1.14.1",
-            "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-            "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-            "requires": {
-                "assert-plus": "^1.0.0"
-            }
-        },
-        "data-urls": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
-            "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
-            "requires": {
-                "abab": "^2.0.3",
-                "whatwg-mimetype": "^2.3.0",
-                "whatwg-url": "^8.0.0"
-            }
-        },
-        "debug": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-            "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
-            "requires": {
-                "ms": "2.0.0"
-            }
-        },
-        "decimal.js": {
-            "version": "10.2.0",
-            "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz",
-            "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw=="
-        },
-        "deep-is": {
-            "version": "0.1.3",
-            "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
-            "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
-        },
-        "delayed-stream": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-            "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
-        },
-        "discord.js": {
-            "version": "12.0.1",
-            "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.0.1.tgz",
-            "integrity": "sha512-lUlrkAWSb5YTB1WpSZHjeUXxGlHK8VDjrlHLEP4lJj+etFAellURpmRYl29OPJ/7arQWB879pP4rvhhzpdOF7w==",
-            "requires": {
-                "@discordjs/collection": "^0.1.5",
-                "abort-controller": "^3.0.0",
-                "form-data": "^3.0.0",
-                "node-fetch": "^2.6.0",
-                "prism-media": "^1.2.0",
-                "setimmediate": "^1.0.5",
-                "tweetnacl": "^1.0.3",
-                "ws": "^7.2.1"
-            }
-        },
-        "domexception": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
-            "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
-            "requires": {
-                "webidl-conversions": "^5.0.0"
-            }
-        },
-        "duckduckgo-images-api": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmjs.org/duckduckgo-images-api/-/duckduckgo-images-api-1.0.4.tgz",
-            "integrity": "sha512-s9iDQMMFeETtfU5Tp5i5GA19XRmU3QrER9EUhwnUdcb76ZgrpLglcQhNiF1G9Gj31LXaUIlki0sa53eRD/FjyA==",
-            "requires": {
-                "axios": "^0.18.0"
-            }
-        },
-        "ecc-jsbn": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-            "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
-            "requires": {
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.1.0"
-            }
-        },
-        "escodegen": {
-            "version": "1.14.1",
-            "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz",
-            "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==",
-            "requires": {
-                "esprima": "^4.0.1",
-                "estraverse": "^4.2.0",
-                "esutils": "^2.0.2",
-                "optionator": "^0.8.1",
-                "source-map": "~0.6.1"
-            }
-        },
-        "esprima": {
-            "version": "4.0.1",
-            "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-            "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
-        },
-        "estraverse": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-            "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
-        },
-        "esutils": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
-        },
-        "event-target-shim": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
-            "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
-        },
-        "extend": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-            "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
-        },
-        "extsprintf": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-            "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
-        },
-        "fast-deep-equal": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
-            "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
-        },
-        "fast-json-stable-stringify": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
-        },
-        "fast-levenshtein": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-            "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
-        },
-        "follow-redirects": {
-            "version": "1.5.10",
-            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
-            "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
-            "requires": {
-                "debug": "=3.1.0"
-            }
-        },
-        "forever-agent": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-            "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
-        },
-        "form-data": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
-            "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
-            "requires": {
-                "asynckit": "^0.4.0",
-                "combined-stream": "^1.0.8",
-                "mime-types": "^2.1.12"
-            }
-        },
-        "fs.realpath": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-            "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
-        },
-        "getpass": {
-            "version": "0.1.7",
-            "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-            "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
-            "requires": {
-                "assert-plus": "^1.0.0"
-            }
-        },
-        "glob": {
-            "version": "7.1.6",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
-            "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
-            "requires": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.0.4",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            }
-        },
-        "har-schema": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-            "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
-        },
-        "har-validator": {
-            "version": "5.1.3",
-            "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
-            "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
-            "requires": {
-                "ajv": "^6.5.5",
-                "har-schema": "^2.0.0"
-            }
-        },
-        "html-encoding-sniffer": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
-            "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==",
-            "requires": {
-                "whatwg-encoding": "^1.0.5"
-            }
-        },
-        "http-signature": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-            "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
-            "requires": {
-                "assert-plus": "^1.0.0",
-                "jsprim": "^1.2.2",
-                "sshpk": "^1.7.0"
-            }
-        },
-        "iconv-lite": {
-            "version": "0.4.24",
-            "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-            "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-            "requires": {
-                "safer-buffer": ">= 2.1.2 < 3"
-            }
-        },
-        "inflight": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-            "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-            "requires": {
-                "once": "^1.3.0",
-                "wrappy": "1"
-            }
-        },
-        "inherits": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
-        },
-        "ip-regex": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
-            "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk="
-        },
-        "is-buffer": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
-            "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
-        },
-        "is-potential-custom-element-name": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
-            "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c="
-        },
-        "is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
-        },
-        "isstream": {
-            "version": "0.1.2",
-            "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-            "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
-        },
-        "jsbn": {
-            "version": "0.1.1",
-            "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-            "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
-        },
-        "jsdom": {
-            "version": "16.2.0",
-            "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.0.tgz",
-            "integrity": "sha512-6VaW3UWyKbm9DFVIAgTfhuwnvqiqlRYNg5Rk6dINTVoZT0eKz+N86vQZr+nqt1ny1lSB1TWZJWSEWQAfu8oTpA==",
-            "requires": {
-                "abab": "^2.0.3",
-                "acorn": "^7.1.0",
-                "acorn-globals": "^4.3.4",
-                "cssom": "^0.4.4",
-                "cssstyle": "^2.2.0",
-                "data-urls": "^2.0.0",
-                "decimal.js": "^10.2.0",
-                "domexception": "^2.0.1",
-                "escodegen": "^1.13.0",
-                "html-encoding-sniffer": "^2.0.0",
-                "is-potential-custom-element-name": "^1.0.0",
-                "nwsapi": "^2.2.0",
-                "parse5": "5.1.1",
-                "request": "^2.88.0",
-                "request-promise-native": "^1.0.8",
-                "saxes": "^4.0.2",
-                "symbol-tree": "^3.2.4",
-                "tough-cookie": "^3.0.1",
-                "w3c-hr-time": "^1.0.1",
-                "w3c-xmlserializer": "^2.0.0",
-                "webidl-conversions": "^5.0.0",
-                "whatwg-encoding": "^1.0.5",
-                "whatwg-mimetype": "^2.3.0",
-                "whatwg-url": "^8.0.0",
-                "ws": "^7.2.1",
-                "xml-name-validator": "^3.0.0"
-            }
-        },
-        "json-schema": {
-            "version": "0.2.3",
-            "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
-            "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
-        },
-        "json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
-        },
-        "json-stringify-safe": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-            "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
-        },
-        "jsprim": {
-            "version": "1.4.1",
-            "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
-            "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
-            "requires": {
-                "assert-plus": "1.0.0",
-                "extsprintf": "1.3.0",
-                "json-schema": "0.2.3",
-                "verror": "1.10.0"
-            }
-        },
-        "levn": {
-            "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
-            "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
-            "requires": {
-                "prelude-ls": "~1.1.2",
-                "type-check": "~0.3.2"
-            }
-        },
-        "lodash": {
-            "version": "4.17.15",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-            "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
-        },
-        "lodash.sortby": {
-            "version": "4.7.0",
-            "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
-            "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
-        },
-        "mime": {
-            "version": "2.4.4",
-            "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
-            "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
-        },
-        "mime-db": {
-            "version": "1.43.0",
-            "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
-            "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
-        },
-        "mime-types": {
-            "version": "2.1.26",
-            "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
-            "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
-            "requires": {
-                "mime-db": "1.43.0"
-            }
-        },
-        "minimatch": {
-            "version": "3.0.4",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-            "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-            "requires": {
-                "brace-expansion": "^1.1.7"
-            }
-        },
-        "ms": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-            "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-        },
-        "node-fetch": {
-            "version": "2.6.0",
-            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
-            "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
-        },
-        "nwsapi": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz",
-            "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ=="
-        },
-        "oauth-sign": {
-            "version": "0.9.0",
-            "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-            "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
-        },
-        "once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "requires": {
-                "wrappy": "1"
-            }
-        },
-        "optionator": {
-            "version": "0.8.3",
-            "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
-            "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
-            "requires": {
-                "deep-is": "~0.1.3",
-                "fast-levenshtein": "~2.0.6",
-                "levn": "~0.3.0",
-                "prelude-ls": "~1.1.2",
-                "type-check": "~0.3.2",
-                "word-wrap": "~1.2.3"
-            }
-        },
-        "parse5": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
-            "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
-        },
-        "path-is-absolute": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
-        },
-        "performance-now": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-            "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
-        },
-        "prelude-ls": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
-            "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
-        },
-        "prism-media": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.1.tgz",
-            "integrity": "sha512-R3EbKwJiYlTvGwcG1DpUt+06DsxOGS5W4AMEHT7oVOjG93MjpdhGX1whHyjnqknylLMupKAsKMEXcTNRbPe6Vw=="
-        },
-        "psl": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
-            "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ=="
-        },
-        "punycode": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
-        },
-        "qs": {
-            "version": "6.5.2",
-            "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-            "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
-        },
-        "request": {
-            "version": "2.88.2",
-            "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
-            "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
-            "requires": {
-                "aws-sign2": "~0.7.0",
-                "aws4": "^1.8.0",
-                "caseless": "~0.12.0",
-                "combined-stream": "~1.0.6",
-                "extend": "~3.0.2",
-                "forever-agent": "~0.6.1",
-                "form-data": "~2.3.2",
-                "har-validator": "~5.1.3",
-                "http-signature": "~1.2.0",
-                "is-typedarray": "~1.0.0",
-                "isstream": "~0.1.2",
-                "json-stringify-safe": "~5.0.1",
-                "mime-types": "~2.1.19",
-                "oauth-sign": "~0.9.0",
-                "performance-now": "^2.1.0",
-                "qs": "~6.5.2",
-                "safe-buffer": "^5.1.2",
-                "tough-cookie": "~2.5.0",
-                "tunnel-agent": "^0.6.0",
-                "uuid": "^3.3.2"
-            },
-            "dependencies": {
-                "form-data": {
-                    "version": "2.3.3",
-                    "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-                    "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-                    "requires": {
-                        "asynckit": "^0.4.0",
-                        "combined-stream": "^1.0.6",
-                        "mime-types": "^2.1.12"
-                    }
-                },
-                "tough-cookie": {
-                    "version": "2.5.0",
-                    "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-                    "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-                    "requires": {
-                        "psl": "^1.1.28",
-                        "punycode": "^2.1.1"
-                    }
-                }
-            }
-        },
-        "request-promise-core": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
-            "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
-            "requires": {
-                "lodash": "^4.17.15"
-            }
-        },
-        "request-promise-native": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz",
-            "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==",
-            "requires": {
-                "request-promise-core": "1.1.3",
-                "stealthy-require": "^1.1.1",
-                "tough-cookie": "^2.3.3"
-            },
-            "dependencies": {
-                "tough-cookie": {
-                    "version": "2.5.0",
-                    "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
-                    "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
-                    "requires": {
-                        "psl": "^1.1.28",
-                        "punycode": "^2.1.1"
-                    }
-                }
-            }
-        },
-        "safe-buffer": {
-            "version": "5.2.0",
-            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
-            "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
-        },
-        "safer-buffer": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-            "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
-        },
-        "saxes": {
-            "version": "4.0.2",
-            "resolved": "https://registry.npmjs.org/saxes/-/saxes-4.0.2.tgz",
-            "integrity": "sha512-EZOTeQ4bgkOaGCDaTKux+LaRNcLNbdbvMH7R3/yjEEULPEmqvkFbFub6DJhJTub2iGMT93CfpZ5LTdKZmAbVeQ==",
-            "requires": {
-                "xmlchars": "^2.2.0"
-            }
-        },
-        "setimmediate": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
-            "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
-        },
-        "source-map": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-            "optional": true
-        },
-        "sshpk": {
-            "version": "1.16.1",
-            "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
-            "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
-            "requires": {
-                "asn1": "~0.2.3",
-                "assert-plus": "^1.0.0",
-                "bcrypt-pbkdf": "^1.0.0",
-                "dashdash": "^1.12.0",
-                "ecc-jsbn": "~0.1.1",
-                "getpass": "^0.1.1",
-                "jsbn": "~0.1.0",
-                "safer-buffer": "^2.0.2",
-                "tweetnacl": "~0.14.0"
-            },
-            "dependencies": {
-                "tweetnacl": {
-                    "version": "0.14.5",
-                    "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-                    "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
-                }
-            }
-        },
-        "stealthy-require": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
-            "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
-        },
-        "symbol-tree": {
-            "version": "3.2.4",
-            "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
-            "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
-        },
-        "tough-cookie": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
-            "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
-            "requires": {
-                "ip-regex": "^2.1.0",
-                "psl": "^1.1.28",
-                "punycode": "^2.1.1"
-            }
-        },
-        "tr46": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
-            "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
-            "requires": {
-                "punycode": "^2.1.1"
-            }
-        },
-        "tslib": {
-            "version": "1.11.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
-            "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
-        },
-        "tunnel-agent": {
-            "version": "0.6.0",
-            "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-            "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-            "requires": {
-                "safe-buffer": "^5.0.1"
-            }
-        },
-        "tweetnacl": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
-            "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
-        },
-        "type-check": {
-            "version": "0.3.2",
-            "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
-            "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
-            "requires": {
-                "prelude-ls": "~1.1.2"
-            }
-        },
-        "typescript": {
-            "version": "3.8.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
-            "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w=="
-        },
-        "unirest": {
-            "version": "0.6.0",
-            "resolved": "https://registry.npmjs.org/unirest/-/unirest-0.6.0.tgz",
-            "integrity": "sha512-BdYdcYJHXACqZ53k8Zz7QlNK/1W/HjCZlmg1OaaN/oTSp4FTWh0upXGSJsG88PljDBpSrNc2R649drasUA9NEg==",
-            "requires": {
-                "form-data": "^0.2.0",
-                "mime": "^2.4.0",
-                "request": "^2.88.0"
-            },
-            "dependencies": {
-                "combined-stream": {
-                    "version": "0.0.7",
-                    "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
-                    "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=",
-                    "requires": {
-                        "delayed-stream": "0.0.5"
-                    }
-                },
-                "delayed-stream": {
-                    "version": "0.0.5",
-                    "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz",
-                    "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8="
-                },
-                "form-data": {
-                    "version": "0.2.0",
-                    "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz",
-                    "integrity": "sha1-Jvi8JtpkQOKZy9z7aQNcT3em5GY=",
-                    "requires": {
-                        "async": "~0.9.0",
-                        "combined-stream": "~0.0.4",
-                        "mime-types": "~2.0.3"
-                    }
-                },
-                "mime-db": {
-                    "version": "1.12.0",
-                    "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
-                    "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc="
-                },
-                "mime-types": {
-                    "version": "2.0.14",
-                    "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
-                    "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=",
-                    "requires": {
-                        "mime-db": "~1.12.0"
-                    }
-                }
-            }
-        },
-        "uri-js": {
-            "version": "4.2.2",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
-            "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
-            "requires": {
-                "punycode": "^2.1.0"
-            }
-        },
-        "uuid": {
-            "version": "3.4.0",
-            "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-            "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
-        },
-        "verror": {
-            "version": "1.10.0",
-            "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-            "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-            "requires": {
-                "assert-plus": "^1.0.0",
-                "core-util-is": "1.0.2",
-                "extsprintf": "^1.2.0"
-            }
-        },
-        "w3c-hr-time": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
-            "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=",
-            "requires": {
-                "browser-process-hrtime": "^0.1.2"
-            }
-        },
-        "w3c-xmlserializer": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz",
-            "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==",
-            "requires": {
-                "xml-name-validator": "^3.0.0"
-            }
-        },
-        "webidl-conversions": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
-            "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA=="
-        },
-        "whatwg-encoding": {
-            "version": "1.0.5",
-            "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
-            "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==",
-            "requires": {
-                "iconv-lite": "0.4.24"
-            }
-        },
-        "whatwg-mimetype": {
-            "version": "2.3.0",
-            "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
-            "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
-        },
-        "whatwg-url": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.0.0.tgz",
-            "integrity": "sha512-41ou2Dugpij8/LPO5Pq64K5q++MnRCBpEHvQr26/mArEKTkCV5aoXIqyhuYtE0pkqScXwhf2JP57rkRTYM29lQ==",
-            "requires": {
-                "lodash.sortby": "^4.7.0",
-                "tr46": "^2.0.0",
-                "webidl-conversions": "^5.0.0"
-            }
-        },
-        "word-wrap": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-            "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
-        },
-        "wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-        },
-        "ws": {
-            "version": "7.2.1",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz",
-            "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A=="
-        },
-        "xml-name-validator": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
-            "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
-        },
-        "xmlchars": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
-            "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
-        }
-    }
-}
diff --git a/package.json b/package.json
@@ -9,12 +9,17 @@
     "license": "GPL-3.0",
     "author": "Sammy et Frens",
     "version": "0.1.0",
+    "engines": {
+        "node": ">=10.0.0"
+    },
     "main": "./build/main.js",
     "types": "./build/main.d.ts",
     "scripts": {
-        "build": "./build.sh && tsc -b ./tsconfig.json",
+        "build": "./build.sh",
+        "reset": "rm -rf ./build ./node_modules ./yarn.lock ./packages-lock.json",
         "start": "node .",
-        "quick": "npm run build && npm run start"
+        "quick": "yarn run build && yarn run start",
+        "deploy": "now && echo \"Remeber to scale: `now scale [url] 1 1`\""
     },
     "homepage": "https://github.com/Demonstrandum/simpomatic",
     "repository": {
@@ -25,17 +30,16 @@
         "url": "https://github.com/Demonstrandum/simpomatic/issues"
     },
     "dependencies": {
-        "typescript": "^3.8.3",
         "@typeit/discord": "1.0.3",
-        "@types/node": "",
-        "@types/ws": "",
+        "@types/node": "^13.7.7",
+        "@types/ws": "^7.2.2",
+        "deepcopy": "^2.0.0",
         "discord.js": "12.0.1",
-        "duckduckgo-images-api": "^1.0.4",
-        "jsdom": "^16.2.0",
         "tslib": "^1.10.0",
+        "typescript": "^3.8.3",
         "unirest": "^0.6.0"
     },
-    "engines": {
-        "node": "13.9.0"
+    "devDependencies": {
+        "now": "^17.0.4"
     }
 }
diff --git a/public/index.html b/public/index.html
@@ -0,0 +1,8 @@
+<html>
+    <head>
+        <title>Simp'O'Matic</title>
+    </head>
+    <body>
+        This is a Discord Bot.
+    </body>
+</html>