commit c7f656f166503a2ee6832b4b89f4c7f95693f8c6
parent 3ed98527d9d9b82b1eec8695aaf1b538ab9fa697
Author: Demonstrandum <moi@knutsen.co>
Date: Mon, 16 Mar 2020 16:49:54 +0000
Added history expansion, but just for commands.
Diffstat:
9 files changed, 371 insertions(+), 79 deletions(-)
diff --git a/HELP.md b/HELP.md
@@ -1,16 +1,35 @@
**KEY:**
+How to read this help page (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.
▬▬▬
-- `!help` — Shows this page.
+- `!help` — Shows help messages.
+ - `!help all` — shows page of help for all commands.
+ - `!help <help>` — shows help on how to use the help command.
+ - `!help key` — shows how to read the help messages.
+ - `!help source` — shows information about the source code for this bot.
+ - `!help [!command]` — shows help on a certain command.
+- `!!` — Expands into the previously issued command:
+ - `!!@ [@user-name]` — Expands into the previously issued command by that user.
+ - `!!^` — Expands into the previous message sent (this counts commands as being messages too, since they are).
+ - `!!^@` — Expands into the previously sent message by that user.
+ - `!!{^,@,^@,_}<ordinal-number>` — Works like all the `!!`-commands, but with an index number to get the nth-to-last message.
+ - **Example:** `!mock !!^@3 @Danny` — Repeats what `@Danny`'s 3rd-from-last message was back to him, but in a mocking way.
+ - **Example:** `!! hello` — Executes the command that had just been executed, but with an extra argument (namely: `hello`).
- `!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.
- `!ping` — Test the response-time/latency of the bot, by observing the time elapsed between the sending of this command, and the subsequent (one-word) response from the bot.
- `!id <who>` — Print ID of user, or self if no-one is specified.
+- `!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.
- `!alias` — Manage aliases to commands:
+ - **Have a look at the aliases list, for alternative 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.
@@ -39,12 +58,33 @@
- `!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.
- `!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.
-- `!news [news-search-term]` — Sends you the most relevant new on the specified topic area.
+- `!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.
- `!say [phrase]` — Repeats what you told it to say.
- `!milkies` — In case you're feeling thirsty...
-- `!cowsay [phrase]` — Make a cow say something.
+- `!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 BaCk_...
+- `!boomer [phrase]` — Say something, but in the way your demented boomer uncle would write it on Facebook.
+
+▬▬▬
+
+**Source Code & Bugs:**
+
+- `!github` — Get GitHub link. (https://github.com/Demonstrandum/Simp-O-Matic)
+- `!fork` — Fork the repository and send me a pull-request for your patches. (https://github.com/Demonstrandum/Simp-O-Matic/fork)
+- `!issue` — Spot a bug, have an issue or want to request a new feature? There's a page for that. (https://github.com/Demonstrandum/Simp-O-Matic/issues)
+
+**Licenced under GNU GPLv3! _Free_ as in Freedom!**
diff --git a/build.sh b/build.sh
@@ -14,4 +14,6 @@ echo "${bold}Compiling TypeScript...${reset}"
./node_modules/.bin/tsc -b ./tsconfig.json
+[ -f "./export_secrets.sh" ] && source ./export_secrets.sh
+
echo -e "\n${bold}Build done.${reset}"
diff --git a/lib/api/web.ts b/lib/api/contextual.ts
diff --git a/lib/default.ts b/lib/default.ts
@@ -10,6 +10,7 @@ export default {
commands: {
prefix: '!',
+ max_history: 100,
not_understood: "Command not understood",
aliases: {
'img': 'image',
@@ -17,6 +18,7 @@ export default {
'h': 'help',
's': 'search',
'yt': 'youtube',
+ 'y': 'youtube',
'd': 'define',
'oed': 'define',
'oxford': 'define',
@@ -26,7 +28,19 @@ export default {
'whitelist': 'ignore whitelist',
'w': 'weather',
'reply': 'respond',
- 'reject': 'delete'
+ 'reject': 'delete',
+ 'wa': 'wolfram',
+ 'wolf': 'wolfram',
+ 'toilet': 'figlet',
+ 'wiki': 'wikipedia',
+ 'aliases': 'alias',
+ 'boomerfy': 'boomer',
+ 'mocking': 'mock',
+ 'pull': 'fork',
+ 'git': 'github',
+ 'bug': 'issue',
+ 'source': 'github',
+ 'save': 'export'
},
},
diff --git a/lib/extensions.ts b/lib/extensions.ts
@@ -1,47 +1,88 @@
// Global Extensions:
declare global {
interface Array<T> {
+ head(): T
+ tail(): Array<T>
+ first(): T
+ last(off? : number | undefined): T
unique(): Array<T>
+ squeeze(): Array<T>
+ each(callbackfn : (value : T, index : number, array : T[]) => void, thisArg? : T): void
mut_unique(): Array<T>
mut_map(f: (T) => any): Array<any>
}
+
interface String {
squeeze(): string
capitalize(): string
+ leading_space(): string
+ head(): string
+ tail(): string
+ first(): string
+ last(off? : number): string
}
+
interface Number {
round_to(dp: number): number
}
-
}
// Array Extensions:
+Array.prototype.head = function () {
+ return this[0];
+};
+
+Array.prototype.first = Array.prototype.head;
+
+Array.prototype.tail = function () {
+ return this.slice(1);
+};
+
+Array.prototype.last = function (off=0) {
+ return this[this.length - 1 - off];
+};
+
Array.prototype.unique = function () {
return this.filter((e, i) => this.indexOf(e) === i)
-}
+};
+
+Array.prototype.squeeze = function () { return this.filter(e => !!e); };
+
+Array.prototype.each = Array.prototype.forEach;
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:
String.prototype.squeeze = function () {
- return this.split(/[ ]+/).join(' ');
+ return this
+ .replace(/[ ]+/g, ' ')
+ .replace(/\n[ ]/g, '\n');
+};
+
+String.prototype.leading_space = function () {
+ return this.replace(/\n[ ]([^ ]+)/g, '\n$1');
};
String.prototype.capitalize = function () {
return this.charAt(0).toUpperCase() + this.slice(1);
-}
+};
+
+String.prototype.head = Array.prototype.head as any;
+String.prototype.tail = Array.prototype.tail as any;
+String.prototype.first = Array.prototype.first as any;
+String.prototype.last = Array.prototype.last as any;
// Number Extensions:
Number.prototype.round_to = function (dp : number) {
diff --git a/lib/format_oed.ts b/lib/format_oed.ts
@@ -0,0 +1,70 @@
+import { Attachment } from 'discord.js';
+import { pp } from './utils';
+
+// Mmm... spaghetti...
+export default (res, message) => {
+ let msg = `Definition for ‘${res.word}’, yielded:\n`;
+ let has_sent_audio = false;
+
+ const lex_entries = res['results'][0].lexicalEntries;
+ let entry_n = 1;
+ for (const lex_entry of lex_entries) {
+ if (lex_entries.length > 1) {
+ msg += `\nLexical Entry №${entry_n}:\n`
+ entry_n += 1;
+ }
+ console.log('Lex entry:', pp(lex_entries))
+ for (const entry of Object.values(lex_entry.entries)) {
+ const senses = entry['senses'];
+
+ for (const sense of Object.values(senses) as any) {
+ let sense_msg = "";
+ if (!!sense.definitions && sense.definitions.length > 0) {
+ for (const definition
+ of Object.values(sense.definitions) as any) {
+ sense_msg += ` Defined as (${lex_entry.lexicalCategory.text.toLowerCase()}):\n> ${definition.capitalize()}\n`;
+ }
+ }
+ if (!!sense.synonyms && sense.synonyms.length > 0) {
+ const synonyms = sense.synonyms
+ .map(s => `‘${s.text}’`)
+ .join(', ');
+ sense_msg += ` Synonyms include: ${synonyms}\n`;
+ }
+ if (sense_msg.trim().length > 0) {
+ msg += "\nIn the sense:\n"
+ msg += sense_msg;
+ }
+ }
+ const etys = entry['etymologies'];
+ if (!!etys && etys.length > 0) {
+ msg += '\nEtymology:\n ';
+ msg += etys.join(';\n ');
+ msg += '\n';
+ }
+ }
+ if (!!lex_entry.pronunciations) {
+ const prons = Object.values(lex_entry.pronunciations) as any;
+ if (!!prons && prons.length > 0) {
+ msg += "\nPronunciations:\n"
+ for (const pron of prons) {
+ msg += ` Dialects of ${pron.dialects.join(', ')}:\n`;
+ msg += ` ${pron.phoneticNotation}: [${pron.phoneticSpelling}]\n`;
+ if (pron.audioFile) {
+ msg += ` Audio file: ${pron.audioFile}\n`;
+ if (!has_sent_audio) {
+ has_sent_audio = !has_sent_audio;
+ const attach = new Attachment(
+ pron.audioFile,
+ pron.audioFile.split('/').slice(-1)[0]
+ );
+ message.channel.send('', attach);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return msg;
+}
diff --git a/lib/main.ts b/lib/main.ts
@@ -1,20 +1,31 @@
+// Don't exit immediately.
+process.stdin.resume();
+
+// Discord Bot API.
import { Discord, On, Client } from '@typeit/discord';
import { Message, Attachment } from 'discord.js';
+// System interaction modules.
import {
readFileSync as read_file,
writeFileSync as write_file,
} from 'fs';
import { execSync as shell } from 'child_process';
-import { inspect } from 'util';
+// Local misc/utility functions.
import './extensions';
import { deep_merge, pp, compile_match, export_config } from './utils';
+import format_oed from './format_oed'; // O.E.D. JSON entry to markdown.
+
+// Default bot configuration JSON.
import DEFAULT_CONFIG from './default';
-import web_search from './api/web';
+
+// API specific modules.
+import web_search from './api/contextual';
import oed_lookup from './api/oxford';
import urban_search from './api/urban';
+
// Anything that hasn't been defined in `bot.json`
// will be taken care of by the defaults.
const CONFIG = deep_merge(
@@ -23,7 +34,7 @@ const CONFIG = deep_merge(
// Precompile all regular-expressions in known places.
['respond', 'reject', 'replace']
- .forEach(name => CONFIG.rules[name].mut_map(compile_match))
+ .each(name => CONFIG.rules[name].mut_map(compile_match))
// Store secrets in an object, retrieved from shell's
// environment variables.
@@ -40,15 +51,18 @@ const help_sections = HELP.toString()
let acc = "";
let new_messages = [];
+// 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;
+// Log where __dirname and cwd are for deployment.
console.log('File/Execution locations:', {
'__dirname': __dirname,
'process.cwd()': process.cwd()
@@ -57,6 +71,7 @@ console.log('File/Execution locations:', {
@Discord
export class SimpOMatic {
private static _client : Client;
+ private _COMMAND_HISTORY : Message[] = [];
constructor() {
console.log('Secrets:', pp(SECRETS));
@@ -72,16 +87,25 @@ export class SimpOMatic {
}
process_command(message : Message) {
- const words = message.content.slice(1).split(' ');
+ this._COMMAND_HISTORY.push(message);
+
+ const content = message.content.trim().squeeze();
+ const words = content.tail().split(' ');
+ const args = words.tail();
let command = words[0];
if (CONFIG.commands.aliases.hasOwnProperty(command))
- command = CONFIG.commands.aliases[command];
- command = command.toLowerCase();
+ command = CONFIG.commands.aliases[command].trim().squeeze();
- const args = words.slice(1);
+ const expanded_command_words = command.split(' ');
+ if (expanded_command_words.length > 1) {
+ // This means the alias has expanded to more than just one word.
+ command = expanded_command_words.shift();
+ expanded_command_words.each(e => args.unshift(e));
+ }
- console.log('Received command: ', [command, args]);
+ command = command.toLowerCase();
+ console.log('Received command:', [command, args]);
switch (command) {
case 'ping': {
@@ -99,6 +123,48 @@ export class SimpOMatic {
console.log(`Replied: ${reply}`);
message.answer(reply);
break;
+ } case 'alias': {
+ const p = CONFIG.commands.prefix;
+
+ if (args.length === 0 || args[0] === 'ls') {
+ const lines = Object.keys(CONFIG.commands.aliases)
+ .map((e, i) => `${i + 1}. \`${p}${e}\` ↦ \`${p}${CONFIG.commands.aliases[e]}\``);
+ message.answer('List of **Aliases**:\n')
+ message.channel.send('**KEY: `Alias` ↦ `Command it maps to`**\n\n'
+ + lines.join('\n'));
+ }
+
+ // Check last:
+ if (args.length > 1) { // Actually aliasing something.
+ args[0] = args[0].trim();
+ args[1] = args[1].trim();
+
+ if (args[0][0] === CONFIG.commands.prefix)
+ args[0] = args[0].tail();
+
+ if (args[1][0] === CONFIG.commands.prefix)
+ args[1] = args[1].tail();
+
+ CONFIG.commands.aliases[args[0]] = args.tail().join(' ');
+ message.channel.send(
+ '**Alias added:**\n >>> ' +
+ `\`${p}${args[0]}\` now maps to \`${p}${args.tail().join(' ')}\``);
+ }
+ break;
+ } case 'prefix': {
+ if (args.length == 1) {
+ if (args[0].length !== 1) {
+ message.answer(`You may only use a prefix that is
+ exactly one character/symbol/grapheme/rune long.`
+ .squeeze());
+ break;
+ }
+ CONFIG.commands.prefix = args[0];
+ message.answer(`Command prefix changed to: \`${CONFIG.commands.prefix}\`.`);
+ break;
+ }
+ message.answer(`Current command prefix is: \`${CONFIG.commands.prefix}\`.`);
+ break;
} case 'search': {
const query = args.join(' ');
@@ -111,8 +177,8 @@ export class SimpOMatic {
message.answer('No such results found.');
return;
}
- message.answer(`Web search for ‘${query}’,
- found: ${res['value'][0].url}`);
+ message.answer(`Web search for ‘${query}’, \
+ found: ${res['value'][0].url}`.squeeze());
}).catch(e => message.answer(`Error fetching results:\n${e}`));
break;
} case 'image': {
@@ -127,8 +193,8 @@ export class SimpOMatic {
message.answer('No such images found.');
return;
}
- message.answer(`Image found for ‘${query}’:
- ${res['value'][0].url}`);
+ message.answer(`Image found for ‘${query}’: \
+ ${res['value'][0].url}`.squeeze());
}).catch(e =>
message.answer(`Error fetching image:\n${e}`));
break;
@@ -149,62 +215,37 @@ export class SimpOMatic {
id: SECRETS.oxford.id,
key: SECRETS.oxford.key
}).then(res => {
- let msg = `Definition for ‘${query}’, yielded:\n`;
-
+ console.log('Dictionary response:', pp(res));
if (!res['results']
- || res['results'].length == 0
- || !res['results'][0].lexicalEntries
- || res['results'][0].lexicalEntries.length == 0
- || res['results'][0].lexicalEntries[0].entries.length == 0
- || res['results'][0].lexicalEntries[0].entries[0].senses.length == 0) {
+ || res['results'].length == 0
+ || !res['results'][0].lexicalEntries
+ || res['results'][0].lexicalEntries.length == 0
+ || res['results'][0].lexicalEntries[0].entries.length == 0
+ || res['results'][0].lexicalEntries[0].entries[0].senses.length == 0) {
message.answer(nasty_reply);
return;
}
+ // Format the dictionary entry as a string.
+ const msg = format_oed(res, message);
+
+ if (msg.length >= 2000) { // This should be rare (try defining `run').
+ let part_msg = "";
+ // This assumes no two lines would ever
+ // amount to more than 2000 characters.
+ for (const line of msg.split(/\n/g))
+ if (part_msg.length + line.length >= 2000) {
+ message.channel.send(part_msg);
+ part_msg = line + '\n';
+ } else { part_msg += line + '\n' }
+ // Send what's left over, and not >2000 characters.
+ message.channel.send(part_msg);
- const entry = res['results'][0].lexicalEntries[0];
- const senses = entry.entries[0].senses;
- console.log('Senses:', pp(senses));
- for (const sense of Object.values(senses) as any) {
- let sense_msg = "";
- if (!!sense.definitions && sense.definitions.length > 0) {
- for (const definition
- of Object.values(sense.definitions) as any) {
- sense_msg += ` Defined as:\n> ${definition.capitalize()}\n`;
- }
- }
- if (!!sense.synonyms && sense.synonyms.length > 0) {
- const synonyms = sense.synonyms
- .map(s => `‘${s.text}’`)
- .join(', ');
- sense_msg += ` Synonyms include: ${synonyms}\n`;
- }
- if (sense_msg.length > 0) {
- msg += "In the sense:\n"
- msg += sense_msg;
- }
- }
- if (!!entry.pronunciations) {
- const prons = Object.values(entry.pronunciations) as any;
- if (!!prons && prons.length > 0) {
- msg += "Pronunciations:\n"
- for (const pron of prons) {
- msg += ` Dialects of ${pron.dialects.join(', ')}:\n`;
- msg += ` ${pron.phoneticNotation}: [${pron.phoneticSpelling}]\n`;
- if (pron.audioFile) {
- msg += ` Audio file: ${pron.audioFile}\n`;
- const attach = new Attachment(
- pron.audioFile,
- pron.audioFile.split('/').slice(-1)[0]
- );
- message.channel.send('', attach);
- }
- }
- }
+ return;
}
message.channel.send(msg);
}).catch(e => {
if (e.status == 404) {
- message.channel.send(nasty_reply);
+ message.channel.send(`That 404'd. ${nasty_reply}`);
} else {
message.channel.send(`Error getting definition:\n${e}`);
}
@@ -237,7 +278,10 @@ export class SimpOMatic {
message.answer(`${(4 + Math.random() * 15).round_to(3)} gallons \
of milkies have been deposited in your mouth.`.squeeze());
break;
- } case 'say': {2
+ } case 'ily': {
+ message.answer('Y-you too...');
+ break;
+ }case 'say': {2
message.answer(`Me-sa says: “${args.join(' ')}”`);
break;
} case 'export': {
@@ -283,6 +327,70 @@ export class SimpOMatic {
// TODO: Process _rules_ appropriately.
}
+ last_message(opts) : string {
+ if (!opts.offset) opts.offset = 1;
+ if (opts.mention) opts.mentioning = opts.mentioning.trim();
+
+ if (opts.command) {
+ let commands = this._COMMAND_HISTORY
+ .filter(m => m.channel.id === opts.channel.id)
+
+ if (opts.mention) commands = commands.filter(m =>
+ m.author.toString() === opts.mentioning);
+
+ const command = commands.last(opts.offset - 1);
+ if (!command) {
+ opts.channel.send('Cannot expand, no such'
+ + ' command exists in history.');
+ return 'EXPANSION_ERROR';
+ }
+ return command.content;
+ }
+
+ // TODO: Deal with non command expansions, i.e. !!^
+ return 'last-message';
+ }
+
+ expand(message : Message) : string {
+ // History expansion with !!, !!@, !!^@, !!<ordinal>, etc.
+ const expansions = message.content
+ .replace(/(!!@?\^?\d*)/g, '@EXP$1@EXP')
+ .replace(/(\s)/g, '@EXP$1@EXP')
+ .split('@EXP').squeeze();
+
+ for (let i = 0; i < expansions.length; ++i) {
+ if (expansions[i].startsWith('!!')) {
+ if (expansions[i].length === 2) { // !! expansion
+ expansions[i] = this.last_message({
+ command: true,
+ channel: message.channel
+ });
+ continue;
+ }
+
+ const [opts, offset] = expansions[i].slice(2)
+ .replace(/(\d+)/, '#$1')
+ .split('#');
+
+ const mention = opts.includes('@');
+ const mentioning = expansions[i + 2];
+ if (mention) expansions[i + 2] = '';
+
+ expansions[i] = this.last_message({
+ command: !opts.includes('^'),
+ mention: mention,
+ mentioning: mentioning,
+ offset: offset || 1,
+ channel: message.channel
+ });
+ }
+ }
+
+ // TODO: Deal with 'EXPANSION_ERROR's in `expansions`.
+
+ return expansions.join('');
+ }
+
@On("message")
async on_message(message : Message, client : Client) {
console.log('Message acknowledged.');
@@ -292,9 +400,13 @@ export class SimpOMatic {
console.log('Message received:', message.content);
const trimmed = message.content.trim();
- if (trimmed[0] === CONFIG.commands.prefix) {
+ message.content = trimmed;
+ message.content = this.expand(message);
+
+ console.log('Expanded message:', message.content);
+
+ if (message.content[0] === CONFIG.commands.prefix) {
console.log('Message type: command.')
- message.content = trimmed;
this.process_command(message);
} else {
console.log('Message type: generic.')
@@ -303,8 +415,19 @@ export class SimpOMatic {
}
}
+const on_termination = () => {
+ // Back-up the resultant CONFIG to an external file.
+ write_file(`${process.cwd()}/export-exit.json`, export_config(CONFIG, {}));
+ console.log('Last config before exit saved! (`export-exit.json`)');
+ process.exit(0);
+};
+
// 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, {}));
+// Handle exits.
+process.on('exit', on_termination);
+process.on('SIGINT', on_termination);
+process.on('SIGUSR1', on_termination);
+process.on('SIGUSR2', on_termination);
+process.on('uncaughtException', on_termination);
diff --git a/lib/utils.ts b/lib/utils.ts
@@ -13,11 +13,11 @@ export const type: (obj: any) => string = (global => obj =>
export const pp = o => inspect(o, {
colors: true,
showHidden: false,
- depth: 50
+ depth: 23
});
export const deep_merge_pair = (target, source) => {
- Object.keys(source).forEach(key => {
+ Object.keys(source).each(key => {
const target_value = target[key];
const source_value = source[key];
diff --git a/package.json b/package.json
@@ -15,10 +15,12 @@
"main": "./build/main.js",
"types": "./build/main.d.ts",
"scripts": {
- "build": "node -v && ./build.sh",
+ "build": "node -v && . ./build.sh",
"reset": "rm -rf ./build ./node_modules ./yarn.lock ./packages-lock.json",
"start": "node .",
- "quick": "yarn run build && yarn run start"
+ "quick": ". ./build.sh && yarn run start",
+ "deploy-scale": "heroku ps:scale service=1 -a simp-o-matic",
+ "deploy-restart": "heroku restart - simp-o-matic"
},
"homepage": "https://github.com/Demonstrandum/simpomatic",
"repository": {