diff --git a/index.ts b/index.ts deleted file mode 100644 index 25d4974..0000000 --- a/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import c from "config"; - -import Conniebot from "./src"; -import commands from "./src/commands"; - -const token = "token"; -const database = "database"; - -if (!c.has(token)) { - throw new TypeError("Couldn't find a token to connect with."); -} - -if (!c.has(database)) { - throw new TypeError("No database filename listed."); -} - -const conniebot = new Conniebot(c.get(token), c.get(database)); -conniebot.registerCommands(commands); diff --git a/src/conniebot.ts b/src/conniebot.ts new file mode 100644 index 0000000..7a4db8d --- /dev/null +++ b/src/conniebot.ts @@ -0,0 +1,157 @@ +import c from "config"; +import { Client, ClientOptions, Message, RichEmbed } from "discord.js"; +import process from "process"; +import OuterXRegExp from "xregexp"; + +import embed from "./embed"; +import ConniebotDatabase from "./helper/db-management"; +import startup from "./helper/startup"; +import { log, messageSummary } from "./helper/utils"; +import x2i from "./x2i"; + +export type CommandCallback = + (this: Conniebot, message: Message, ...args: string[]) => Promise; + +export interface ICommands { + [key: string]: CommandCallback; +} + +export default class Conniebot { + public bot: Client; + public db: ConniebotDatabase; + private commands: ICommands; + + constructor(token: string, dbFile: string, clientOptions?: ClientOptions) { + log("verbose", "Starting to load bot..."); + + this.bot = new Client(clientOptions); + this.db = new ConniebotDatabase(dbFile); + this.commands = {}; + + this.bot.on("ready", () => startup(this.bot, this.db)) + .on("message", this.parse) + .on("error", err => { + if (err && err.message && err.message.includes("ECONNRESET")) { + return log("warn", "connection reset. oops!"); + } + this.panicResponsibly(err); + }) + .login(token); + + process.once("uncaughtException", this.panicResponsibly); + } + + /** + * Record the error and proceed to crash. + * + * @param err The error to catch. + * @param exit Should exit? (eg ECONNRESET would not require reset) + */ + private panicResponsibly = async (err: any, exit = true) => { + log("error", err); + await this.db.addError(err); + if (exit) { + process.exit(1); + } + } + + /** + * Looks for a reply message. + * + * @param message Received message. + */ + private async command(message: Message) { + // commands + const prefixRegex = OuterXRegExp.build( + `(?:^${OuterXRegExp.escape(c.get("prefix"))})(\\S*) ?(.*)`, [], + ); + + const toks = message.content.match(prefixRegex); + if (!toks) return; + const [, cmd, args] = toks; + + // assume that command has already been bound + // no way currently to express this without clearing the types + const cb: any = this.commands[cmd]; + if (!cb) return; + + try { + const logItem = await cb(message, ...args.split(" ")); + log(`success:command/${cmd}`, logItem); + } catch (err) { + log(`error:command/${cmd}`, err); + } + } + + /** + * Sends an x2i string (but also could be used for simple embeds) + * + * @param message Message to reply to + */ + private async x2iExec(message: Message) { + let results = x2i(message.content); + const parsed = Boolean(results && results.length !== 0); + if (parsed) { + const response = new RichEmbed().setColor( + c.get("embeds.colors.success"), + ); + let logCode = "all"; + + // check timeout + const charMax = parseInt(c.get("embeds.timeoutChars"), 10); + if (results.length > charMax) { + results = `${results.slice(0, charMax - 1)}…`; + + response + .addField("Timeout", c.get("embeds.timeoutMessage")) + .setColor(c.get("embeds.colors.warning")); + + logCode = "partial"; + } + + response.setDescription(results); + + const respond = (stat: string, ...ms: any[]) => + log(`${stat}:x2i/${logCode}`, messageSummary(message), ...ms); + + try { + await embed(message.channel, response); + respond("success"); + } catch (err) { + respond("error", err); + } + } + + return parsed; + } + + /** + * Acts for a response to a message. + * + * @param message Message to parse for responses + */ + protected parse = async (message: Message) => { + if (message.author.bot) return; + if (await this.x2iExec(message)) return; + await this.command(message); + } + + /** + * Register multiple commands at once. + */ + public registerCommands(callbacks: ICommands) { + for (const [name, cmd] of Object.entries(callbacks)) { + this.register(name, cmd); + } + } + + /** + * Register a single custom command. + * + * @param command Command name that comes after prefix. Name must be `\S+`. + * @param callback Callback upon seeing the name. `this` will be bound automatically. + */ + public register(command: string, callback: CommandCallback) { + this.commands[command] = callback.bind(this); + } +} diff --git a/src/embed.ts b/src/embed.ts index 17a578c..934aca1 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -1,7 +1,7 @@ import c from "config"; import { Channel, RichEmbed } from "discord.js"; -import { isTextChannel } from "./utils"; +import { isTextChannel } from "./helper/utils"; /** * Grabs body from RichEmbed, optionally discarding headers. diff --git a/src/help.ts b/src/help.ts index df4a126..2001f72 100644 --- a/src/help.ts +++ b/src/help.ts @@ -2,7 +2,7 @@ import c from "config"; import { Channel, RichEmbed, User } from "discord.js"; import embed from "./embed"; -import { isTextChannel } from "./utils"; +import { isTextChannel } from "./helper/utils"; const helpMessage: [string, string][] = [ ["x,z,p[phonetic] or x,z,p/phonemic/", diff --git a/src/commands.ts b/src/helper/commands.ts similarity index 96% rename from src/commands.ts rename to src/helper/commands.ts index ed03984..188c805 100644 --- a/src/commands.ts +++ b/src/helper/commands.ts @@ -1,7 +1,7 @@ import c from "config"; -import { ICommands } from "."; -import help from "./help"; +import { ICommands } from "../conniebot"; +import help from "../help"; import { log } from "./utils"; /** diff --git a/src/db-management.ts b/src/helper/db-management.ts similarity index 100% rename from src/db-management.ts rename to src/helper/db-management.ts diff --git a/src/startup.ts b/src/helper/startup.ts similarity index 100% rename from src/startup.ts rename to src/helper/startup.ts diff --git a/src/utils.ts b/src/helper/utils.ts similarity index 90% rename from src/utils.ts rename to src/helper/utils.ts index 17bb35d..ddcd64f 100644 --- a/src/utils.ts +++ b/src/helper/utils.ts @@ -6,7 +6,12 @@ import { Channel, Message, TextChannel } from "discord.js"; import npmlog from "npmlog"; // init log style -Object.defineProperty(npmlog, "heading", { get: () => `[${new Date().toISOString()}]` }); +Object.defineProperty(npmlog, "heading", { + get: () => `[${new Date().toISOString()}]`, + /* tslint:disable:no-empty */ + set: () => {}, // ignore sets since we just need it to be a timestamp + /* tslint:enable:no-empty */ +}); npmlog.headingStyle = { fg: "blue" }; npmlog.levels = new Proxy(npmlog.levels, { get: (o, k) => o[k] || o.info, diff --git a/src/index.ts b/src/index.ts index eefa736..f152f2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,157 +1,18 @@ import c from "config"; -import { Client, ClientOptions, Message, RichEmbed } from "discord.js"; -import process from "process"; -import OuterXRegExp from "xregexp"; -import ConniebotDatabase from "./db-management"; -import embed from "./embed"; -import startup from "./startup"; -import { log, messageSummary } from "./utils"; -import x2i from "./x2i"; +import Conniebot from "./conniebot"; +import commands from "./helper/commands"; -export type CommandCallback = - (this: Conniebot, message: Message, ...args: string[]) => Promise; +const token = "token"; +const database = "database"; -export interface ICommands { - [key: string]: CommandCallback; +if (!c.has(token)) { + throw new TypeError("Couldn't find a token to connect with."); } -export default class Conniebot { - public bot: Client; - public db: ConniebotDatabase; - private commands: ICommands; - - constructor(token: string, dbFile: string, clientOptions?: ClientOptions) { - log("verbose", "Starting to load bot..."); - - this.bot = new Client(clientOptions); - this.db = new ConniebotDatabase(dbFile); - this.commands = {}; - - this.bot.on("ready", () => startup(this.bot, this.db)) - .on("message", this.parse) - .on("error", err => { - if (err && err.message && err.message.includes("ECONNRESET")) { - return log("warn", "connection reset. oops!"); - } - this.panicResponsibly(err); - }) - .login(token); - - process.once("uncaughtException", this.panicResponsibly); - } - - /** - * Record the error and proceed to crash. - * - * @param err The error to catch. - * @param exit Should exit? (eg ECONNRESET would not require reset) - */ - private panicResponsibly = async (err: any, exit = true) => { - log("error", err); - await this.db.addError(err); - if (exit) { - process.exit(1); - } - } - - /** - * Looks for a reply message. - * - * @param message Received message. - */ - private async command(message: Message) { - // commands - const prefixRegex = OuterXRegExp.build( - `(?:^${OuterXRegExp.escape(c.get("prefix"))})(\\S*) ?(.*)`, [], - ); - - const toks = message.content.match(prefixRegex); - if (!toks) return; - const [, cmd, args] = toks; - - // assume that command has already been bound - // no way currently to express this without clearing the types - const cb: any = this.commands[cmd]; - if (!cb) return; - - try { - const logItem = await cb(message, ...args.split(" ")); - log(`success:command/${cmd}`, logItem); - } catch (err) { - log(`error:command/${cmd}`, err); - } - } - - /** - * Sends an x2i string (but also could be used for simple embeds) - * - * @param message Message to reply to - */ - private async x2iExec(message: Message) { - let results = x2i(message.content); - const parsed = Boolean(results && results.length !== 0); - if (parsed) { - const response = new RichEmbed().setColor( - c.get("embeds.colors.success"), - ); - let logCode = "all"; - - // check timeout - const charMax = parseInt(c.get("embeds.timeoutChars"), 10); - if (results.length > charMax) { - results = `${results.slice(0, charMax - 1)}…`; - - response - .addField("Timeout", c.get("embeds.timeoutMessage")) - .setColor(c.get("embeds.colors.warning")); - - logCode = "partial"; - } - - response.setDescription(results); - - const respond = (stat: string, ...ms: any[]) => - log(`${stat}:x2i/${logCode}`, messageSummary(message), ...ms); - - try { - await embed(message.channel, response); - respond("success"); - } catch (err) { - respond("error", err); - } - } - - return parsed; - } - - /** - * Acts for a response to a message. - * - * @param message Message to parse for responses - */ - protected parse = async (message: Message) => { - if (message.author.bot) return; - if (await this.x2iExec(message)) return; - await this.command(message); - } - - /** - * Register multiple commands at once. - */ - public registerCommands(callbacks: ICommands) { - for (const [name, cmd] of Object.entries(callbacks)) { - this.register(name, cmd); - } - } - - /** - * Register a single custom command. - * - * @param command Command name that comes after prefix. Name must be `\S+`. - * @param callback Callback upon seeing the name. `this` will be bound automatically. - */ - public register(command: string, callback: CommandCallback) { - this.commands[command] = callback.bind(this); - } +if (!c.has(database)) { + throw new TypeError("No database filename listed."); } + +const conniebot = new Conniebot(c.get(token), c.get(database)); +conniebot.registerCommands(commands); diff --git a/src/x2i.ts b/src/x2i.ts index c67972f..cf84091 100644 --- a/src/x2i.ts +++ b/src/x2i.ts @@ -5,7 +5,7 @@ import c from "config"; import yaml from "js-yaml"; import OuterXRegExp from "xregexp"; -import { log, resolveDatapath } from "./utils"; +import { log, resolveDatapath } from "./helper/utils"; interface IRawReplaceKey { raw: ReplaceKey; diff --git a/tsconfig.json b/tsconfig.json index 8d236b6..9edba83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,5 +6,6 @@ "noImplicitAny": true, "esModuleInterop": true, "moduleResolution": "node" - } + }, + "include": ["src"] }