From e4c788ecfa719d390396195dbf3c267faebd3fce Mon Sep 17 00:00:00 2001 From: Jon Deaves Date: Tue, 18 Aug 2020 12:53:56 +0100 Subject: [PATCH] refactor: remove DI framework --- .eslintrc.js | 2 + package.json | 2 +- src/app.ts | 65 ++++++--- src/bot/Bot.ts | 128 ++++++++++++++---- src/bot/commands/Command.ts | 46 +++++++ src/bot/commands/ICommand.ts | 19 --- src/bot/commands/add-greeting.command.ts | 39 ++++++ src/bot/commands/addgreeting.ts | 56 -------- src/bot/commands/character.command.ts | 18 +++ src/bot/commands/character.ts | 40 ------ .../{8ball.ts => eight-ball.command.ts} | 17 +-- src/bot/commands/help.command.ts | 70 ++++++++++ src/bot/commands/help.ts | 68 ---------- src/bot/commands/index.ts | 9 -- src/bot/commands/ping.command.ts | 8 ++ src/bot/commands/ping.ts | 13 -- .../commands/{quotes.ts => quotes.command.ts} | 57 +++----- src/bot/commands/see.command.ts | 10 ++ src/bot/commands/see.ts | 21 --- src/core/services/config.service.ts | 3 - src/core/services/database.service.ts | 35 +++-- src/core/services/http.service.ts | 11 +- src/core/services/logger.service.ts | 8 +- src/core/services/mongo.service.ts | 36 ++--- src/core/types/Dependencies.ts | 13 ++ src/core/types/Quote.ts | 11 ++ src/inversity.config.ts | 20 --- src/main.ts | 31 ++--- src/tools/quotes/collect.ts | 6 +- yarn.lock | 10 +- 30 files changed, 455 insertions(+), 417 deletions(-) create mode 100644 src/bot/commands/Command.ts delete mode 100644 src/bot/commands/ICommand.ts create mode 100644 src/bot/commands/add-greeting.command.ts delete mode 100644 src/bot/commands/addgreeting.ts create mode 100644 src/bot/commands/character.command.ts delete mode 100644 src/bot/commands/character.ts rename src/bot/commands/{8ball.ts => eight-ball.command.ts} (77%) create mode 100644 src/bot/commands/help.command.ts delete mode 100644 src/bot/commands/help.ts delete mode 100644 src/bot/commands/index.ts create mode 100644 src/bot/commands/ping.command.ts delete mode 100644 src/bot/commands/ping.ts rename src/bot/commands/{quotes.ts => quotes.command.ts} (71%) create mode 100644 src/bot/commands/see.command.ts delete mode 100644 src/bot/commands/see.ts create mode 100644 src/core/types/Dependencies.ts create mode 100644 src/core/types/Quote.ts delete mode 100644 src/inversity.config.ts diff --git a/.eslintrc.js b/.eslintrc.js index 0650bfc..05e53f9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,8 @@ module.exports = { 'unicorn/no-fn-reference-in-iterator': 'off', 'unicorn/no-nested-ternary': 'off', 'no-underscore-dangle': ['error', { allowAfterThis: true }], + 'class-methods-use-this': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', }, overrides: [ { diff --git a/package.json b/package.json index 7880f04..ca88d53 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "axios": "^0.19.2", "discord.js": "~12.2.0", "dotenv": "~8.2.0", - "inversify": "~5.0.1", "mongodb": "~3.6.0", "pg": "^8.3.0", "reflect-metadata": "~0.1.13", @@ -55,6 +54,7 @@ "@types/mongodb": "~3.5.25", "@types/node": "~14.0.27", "@types/reflect-metadata": "^0.1.0", + "@types/shortid": "^0.0.29", "@types/sinon": "^9.0.4", "@types/winston": "~2.4.4", "@typescript-eslint/eslint-plugin": "^3.6.1", diff --git a/src/app.ts b/src/app.ts index 74fc7d6..3308045 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,44 +1,65 @@ import { exit } from 'process'; -import container from './inversity.config'; - import ConfigService from './core/services/config.service'; import DatabaseService from './core/services/database.service'; +import HttpService from './core/services/http.service'; import LoggerService from './core/services/logger.service'; import MongoService from './core/services/mongo.service'; +import Dependencies from './core/types/Dependencies'; + import Bot from './bot/Bot'; export default class App { - private _configService: ConfigService = container.resolve(ConfigService); + private _dependencies: Dependencies; - private _loggerService: LoggerService = container.resolve(LoggerService); + // eslint-disable-next-line class-methods-use-this + public async start(): Promise { + await this.loadDependencies(); - private _mongoService: MongoService = container.resolve(MongoService); - - private _databaseService: DatabaseService = container.resolve(DatabaseService); - - private _bot: Bot; - - public async init(): Promise { - try { - await this._mongoService.connect(); - await this._databaseService.connect(); - } catch (error) { - this._loggerService.log('error', 'Cannot connect to database, exiting.', { error }); - exit(1); - } + const bot = new Bot(this._dependencies); try { - this._bot = new Bot(this._configService, this._loggerService, this._mongoService, this._databaseService); - await this._bot.bind(); + await bot.bind(); + this._dependencies.loggerService.log('info', 'Application started'); } catch { exit(1); } } + private async loadDependencies(): Promise { + // Create the services + const configService = new ConfigService(); + const loggerService = new LoggerService(configService); + const databaseService = new DatabaseService(configService, loggerService); + const httpService = new HttpService(loggerService); + const mongoService = new MongoService(configService, loggerService); + + // Load the async stuff + if (!(await databaseService.connect())) { + exit(1); + } + + if (!(await mongoService.connect())) { + exit(1); + } + + this._dependencies = { + configService, + databaseService, + httpService, + loggerService, + mongoService, + }; + } + public exit(): void { - this._mongoService.disconnect(); - this._databaseService.disconnect(); + if (this._dependencies.mongoService) { + this._dependencies.mongoService.disconnect(); + } + + if (this._dependencies.databaseService) { + this._dependencies.databaseService.disconnect(); + } } } diff --git a/src/bot/Bot.ts b/src/bot/Bot.ts index 977df7c..de0483b 100644 --- a/src/bot/Bot.ts +++ b/src/bot/Bot.ts @@ -1,37 +1,106 @@ import Discord from 'discord.js'; -import ConfigService from '../core/services/config.service'; -import DatabaseService from '../core/services/database.service'; -import LoggerService from '../core/services/logger.service'; -import MongoService from '../core/services/mongo.service'; +import Dependencies from '../core/types/Dependencies'; -import rawCommands from './commands'; -import ICommand from './commands/ICommand'; +import Command from './commands/Command'; +import AddGreetingCommand from './commands/add-greeting.command'; +import CharacterCommand from './commands/character.command'; +import EightBallCommand from './commands/eight-ball.command'; +import HelpCommand from './commands/help.command'; +import PingCommand from './commands/ping.command'; +import SeeCommand from './commands/see.command'; +import QuotesCommand from './commands/quotes.command'; export default class Bot { private _discordClient: Discord.Client; - private _commandList: Discord.Collection; + private _commandList: Discord.Collection; - constructor( - private _configService: ConfigService, - private _loggerService: LoggerService, - private _mongoService: MongoService, - private _databaseService: DatabaseService, - ) { + constructor(private _dependencies: Dependencies) { this._discordClient = new Discord.Client(); - this._commandList = new Discord.Collection(); + this._commandList = new Discord.Collection(); + } + + private setCommands(): void { + const prefix = this._dependencies.configService.get('BOT_TRIGGER'); + + // Load in our commands for the command handler + // TODO: Refactor this into a CommandHandler class? + const addGreetingCmd = new AddGreetingCommand( + this._dependencies, + 'addgreeting', + ['ag'], + 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.', + [`\`${prefix}addgreeting Welcome to the club {name}\``], + ); + const characterCmd = new CharacterCommand( + this._dependencies, + 'character', + ['c'], + 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.', + [`\`${prefix}addgreeting Welcome to the club {name}\``], + ); + const eightBallCmd = new EightBallCommand( + this._dependencies, + '8ball', + ['eightball', 'magicball', 'ball', 'wisdomball'], + 'Ask the magic eightball for advice.', + [`\`${prefix} 8ball will I be awesome today?\``], + ); + const helpCmd = new HelpCommand( + this._dependencies, + 'help', + ['commands'], + 'Lists available commands and their usage.', + [`\`${prefix}help\``, `\`${prefix}help ping\``], + ); + const pingCmd = new PingCommand( + this._dependencies, + 'ping', + ['hello'], + 'Responds, kind of like telling you the bot is alive.', + [`\`${prefix}ping\``], + ); + const seeCmd = new SeeCommand( + this._dependencies, + 'see', + ['me'], + 'Sends a DM telling you information about your user on given server.', + [`\`${prefix}see\``], + ); + const quoteCmd = new QuotesCommand( + this._dependencies, + 'quotes', + ['quote', 'quotes', 'q'], + 'Get random quotes from people in chat, or add quotes to the list.', + [ + `\`${prefix}quote @author quote\` or \`${prefix}quote add @author quote\` - Adds "quote" by @author. @author can be Discord mention or any string that terminates with ' ' (space).`, + `\`${prefix}quote search key words\` - Search "key words" and give you results of that quote.`, + `\`${prefix}quote\` (with no arguments) - gets a random quote.`, + `\`${prefix}quote #quoteId\` - Gets specific quote with ID #quoteId`, + ], + ); + + this._commandList.set(addGreetingCmd.name, addGreetingCmd); + this._commandList.set(characterCmd.name, characterCmd); + this._commandList.set(eightBallCmd.name, eightBallCmd); + this._commandList.set(pingCmd.name, pingCmd); + this._commandList.set(seeCmd.name, seeCmd); + this._commandList.set(helpCmd.name, helpCmd); + this._commandList.set(quoteCmd.name, quoteCmd); + + // Set custom data on commands + this._commandList.get('help').commandData = { + commandList: this._commandList, + prefix, + }; } /** * Binds event listeners and connects to the server. */ public async bind(): Promise { - // Load in our commands for the command handler - // TODO: Refactor this into a CommandHandler class? - rawCommands.forEach((rawCommand) => { - this._commandList.set(rawCommand.name, rawCommand); - }); + this.setCommands(); // Bind our events this._discordClient.once('ready', this.onReady.bind(this)); // Triggers once after connecting to server @@ -40,30 +109,33 @@ export default class Bot { // Perform connect, throw the error if we can't try { - await this._discordClient.login(this._configService.get('DISCORD_BOT_TOKEN')); + await this._discordClient.login(this._dependencies.configService.get('DISCORD_BOT_TOKEN')); } catch (error) { - const errMsg = `Cannot initialise Discord client. Check the token: ${this._configService.get( + const errMsg = `Cannot initialise Discord client. Check the token: ${this._dependencies.configService.get( 'DISCORD_BOT_TOKEN', )}`; - this._loggerService.log('error', errMsg, { error }); + this._dependencies.loggerService.log('error', errMsg, { error }); throw new Error(errMsg); } } private onReady(): void { - this._loggerService.log('info', 'Venom is connected to the Discord server'); + this._dependencies.loggerService.log('info', 'Venom is connected to the Discord server'); } private async onMessage(message: Discord.Message): Promise { - const prefix = this._configService.get('BOT_TRIGGER'); + const prefix = this._dependencies.configService.get('BOT_TRIGGER'); // If the message either doesn't start with the prefix or was sent by a bot, exit early. - if (!message.content.toLowerCase().startsWith(prefix.toLowerCase()) || message.author.bot) return; + if (!message.content.toLowerCase().startsWith(prefix.toLowerCase()) || message.author.bot) { + return; + } const args = message.content.slice(prefix.length).trim().split(/ +/); const commandName = args.shift().toLowerCase(); + const command = this._commandList.get(commandName) || this._commandList.find((cmd) => cmd.aliases && cmd.aliases.includes(commandName)); @@ -72,9 +144,11 @@ export default class Bot { message.reply("looks like I haven't learned that trick yet!"); } else { try { - await command.execute(message, args, prefix, this._commandList, this._mongoService, this._databaseService); + // await command.execute(message, args, prefix, this._commandList, this._mongoService, this._databaseService); + + await command.execute(message, args); } catch (error) { - this._loggerService.log('error', error.message); + this._dependencies.loggerService.log('error', error.message); message.reply('there was an error trying to follow that command!'); } } diff --git a/src/bot/commands/Command.ts b/src/bot/commands/Command.ts new file mode 100644 index 0000000..22979b2 --- /dev/null +++ b/src/bot/commands/Command.ts @@ -0,0 +1,46 @@ +import Discord from 'discord.js'; + +import Dependencies from '../../core/types/Dependencies'; + +export default abstract class Command { + /** + * The main trigger word for the command + */ + public name: string; + + /** + * Alternative trigger words for the command + */ + public aliases: string[]; + + /** + * Information about the command, useful for use in the help command + */ + public description: string; + + /** + * A sample of how to use the command, useful for use in the help command + */ + public examples: string[]; + + /** + * Services this command can make use of + */ + protected dependencies: Dependencies; + + /** + * Extra data that the command is storing + */ + public commandData: { [x: string]: unknown }; + + constructor(dependencies: Dependencies, name: string, aliasis: string[], description: string, examples: string[]) { + this.dependencies = dependencies; + this.name = name; + this.aliases = aliasis; + this.description = description; + this.examples = examples; + this.commandData = {}; + } + + abstract async execute(message: Discord.Message, args: string[]): Promise; +} diff --git a/src/bot/commands/ICommand.ts b/src/bot/commands/ICommand.ts deleted file mode 100644 index 481c8dd..0000000 --- a/src/bot/commands/ICommand.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Discord, { Collection } from 'discord.js'; - -import MongoService from '../../core/services/mongo.service'; -import DatabaseService from '../../core/services/database.service'; - -export default interface ICommand { - name: string; - aliases?: string[]; - description: string; - example?: string; - execute: ( - message: Discord.Message, - args: string[], - prefix?: string, - commands?: Collection, - mongoService?: MongoService, - databaseService?: DatabaseService, - ) => Promise; -} diff --git a/src/bot/commands/add-greeting.command.ts b/src/bot/commands/add-greeting.command.ts new file mode 100644 index 0000000..b7c9b84 --- /dev/null +++ b/src/bot/commands/add-greeting.command.ts @@ -0,0 +1,39 @@ +import Discord from 'discord.js'; +import Command from './Command'; + +export default class AddGreetingCommand extends Command { + async execute(message: Discord.Message, args: string[]): Promise { + // Only certain users can use this command + // TODO: Better handling of permissions for commands in a generic way + const permittedRoles = new Set(['staff', 'mod', 'bot-devs']); + const isPermitted = message.member.roles.cache.some((r) => permittedRoles.has(r.name)); + + if (!isPermitted) { + return message.author.send("Sorry but I can't let you add greetings!"); + } + + // Can't do much without a message + if (args.length === 0) { + return message.author.send('When adding a greeting you need to also provide a message!'); + } + + // Check for dupes + const greetingStr = args.join(' '); + const matchedMessages = await this.dependencies.mongoService.find(message.author.id, 'greetings', { + message: greetingStr, + }); + if (matchedMessages.length > 0) { + return message.author.send('That greeting has already been added!'); + } + + const result = await this.dependencies.mongoService.insert(message.author.id, 'greetings', [ + { message: greetingStr }, + ]); + + if (!result) { + return message.author.send("Uh-oh! Couldn't add that greeting!"); + } + + return message.author.send("I've added the greeting you told me about!"); + } +} diff --git a/src/bot/commands/addgreeting.ts b/src/bot/commands/addgreeting.ts deleted file mode 100644 index bb7a567..0000000 --- a/src/bot/commands/addgreeting.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Discord, { Collection } from 'discord.js'; - -import ConfigService from '../../core/services/config.service'; -import MongoService from '../../core/services/mongo.service'; - -import container from '../../inversity.config'; - -import ICommand from './ICommand'; - -const prefix = container.resolve(ConfigService).get('BOT_TRIGGER'); - -const command: ICommand = { - name: 'addgreeting', - aliases: ['ag'], - description: - 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.', - example: `\`${prefix}addgreeting Welcome to the club {name}\``, - async execute( - message: Discord.Message, - args: string[], - _prefix?: string, - _commands?: Collection, - dbService?: MongoService, - ) { - // Only certain users can use this command - // TODO: Better handling of permissions for commands in a generic way - const permittedRoles = new Set(['staff', 'mod', 'bot-devs']); - const isPermitted = message.member.roles.cache.some((r) => permittedRoles.has(r.name)); - - if (!isPermitted) { - return message.author.send("Sorry but I can't let you add greetings!"); - } - - // Can't do much without a message - if (args.length === 0) { - return message.author.send('When adding a greeting you need to also provide a message!'); - } - - // Check for dupes - const greetingStr = args.join(' '); - const matchedMessages = await dbService.find(message.author.id, 'greetings', { message: greetingStr }); - if (matchedMessages.length > 0) { - return message.author.send('That greeting has already been added!'); - } - - const result = await dbService.insert(message.author.id, 'greetings', [{ message: greetingStr }]); - - if (!result) { - return message.author.send("Uh-oh! Couldn't add that greeting!"); - } - - return message.author.send("I've added the greeting you told me about!"); - }, -}; - -export default command; diff --git a/src/bot/commands/character.command.ts b/src/bot/commands/character.command.ts new file mode 100644 index 0000000..04898e4 --- /dev/null +++ b/src/bot/commands/character.command.ts @@ -0,0 +1,18 @@ +import Discord from 'discord.js'; + +import Character from '../../carp/character/character.entity'; + +import Command from './Command'; + +export default class CharacterCommand extends Command { + async execute(message: Discord.Message): Promise { + // Just testing db stuff + const matchedChar = await this.dependencies.databaseService.manager.findOne(Character, message.author.id); + + if (!matchedChar) { + return message.reply(`Doesn't look like you have joined this campaign`); + } + + return message.reply(`Welcome back ${matchedChar.name}`); + } +} diff --git a/src/bot/commands/character.ts b/src/bot/commands/character.ts deleted file mode 100644 index 5c667f1..0000000 --- a/src/bot/commands/character.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Discord, { Collection } from 'discord.js'; - -import ConfigService from '../../core/services/config.service'; -import DatabaseService from '../../core/services/database.service'; -import MongoService from '../../core/services/mongo.service'; - -import container from '../../inversity.config'; - -import Character from '../../carp/character/character.entity'; - -import ICommand from './ICommand'; - -const prefix = container.resolve(ConfigService).get('BOT_TRIGGER'); - -const command: ICommand = { - name: 'character', - aliases: ['c'], - description: - 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.', - example: `\`${prefix}addgreeting Welcome to the club {name}\``, - async execute( - message: Discord.Message, - args: string[], - _prefix?: string, - _commands?: Collection, - _mongoService?: MongoService, - dbService?: DatabaseService, - ) { - // Just testing db stuff - const matchedChar = await dbService.manager.findOne(Character, message.author.id); - - if (!matchedChar) { - return message.reply(`Doesn't look like you have joined this campaign`); - } - - return message.reply(`Welcome back ${matchedChar.name}`); - }, -}; - -export default command; diff --git a/src/bot/commands/8ball.ts b/src/bot/commands/eight-ball.command.ts similarity index 77% rename from src/bot/commands/8ball.ts rename to src/bot/commands/eight-ball.command.ts index 0335052..91c6f84 100644 --- a/src/bot/commands/8ball.ts +++ b/src/bot/commands/eight-ball.command.ts @@ -1,12 +1,8 @@ import Discord from 'discord.js'; +import Command from './Command'; -import ICommand from './ICommand'; - -const command: ICommand = { - name: '8ball', - aliases: ['eightball', 'magicball', 'ball', 'wisdomball'], - description: 'Ask the magic eightball for advice!', - async execute(message: Discord.Message, args: string[]) { +export default class EightBallCommand extends Command { + async execute(message: Discord.Message, args: string[]): Promise { if (args.length === 0) { return message.reply("where's the question?"); } @@ -37,8 +33,7 @@ const command: ICommand = { 'yes - definitely.', 'yeah, you can rely on it.', ]; - return message.reply(responses[Math.floor(Math.random() * responses.length - 1)]); - }, -}; -export default command; + return message.reply(responses[Math.floor(Math.random() * responses.length - 1)]); + } +} diff --git a/src/bot/commands/help.command.ts b/src/bot/commands/help.command.ts new file mode 100644 index 0000000..c098374 --- /dev/null +++ b/src/bot/commands/help.command.ts @@ -0,0 +1,70 @@ +import Discord from 'discord.js'; +import Command from './Command'; + +export default class HelpCommand extends Command { + public commandData: { + commandList: Discord.Collection; + prefix: string; + }; + + async execute(message: Discord.Message, args: string[]): Promise { + const data = []; + + if (!args || args.length === 0) { + // Get for all commands + data.push("here's a list of all my commands:\n"); + + const cmds = this.commandData.commandList.map((c) => c.name); + cmds.forEach((element) => { + const cmd = + this.commandData.commandList.get(element) || + this.commandData.commandList.find((c) => c.aliases && c.aliases.includes(element)); + let response = `\`${this.commandData.prefix}${cmd.name}\` `; + if (cmd.description) { + response += `**${cmd.description}** `; + } + if (cmd.aliases) { + response += `\n\t\t\t*alternatively:* \`${this.commandData.prefix}${cmd.aliases.join( + `\`, \`${this.commandData.prefix}`, + )}\``; + } + data.push(response); + + cmd.examples.forEach((example) => { + data.push(`\t\t\t*for example:* ${example}`); + }); + + data.push('\n'); + }); + data.push(`You can send \`${this.commandData.prefix}help [command name]\` to get info on a specific command!`); + } else { + // Get description of single command + const name = args[0].toLowerCase(); + const cmd = + this.commandData.commandList.get(name) || + this.commandData.commandList.find((c) => c.aliases && c.aliases.includes(name)); + + if (!cmd) { + message.reply("that's not a valid command!"); + } else { + data.push(`**Name:** ${cmd.name}`); + + if (cmd.aliases) { + data.push(`**Aliases:** ${cmd.aliases.join(', ')}`); + } + + if (cmd.description) { + data.push(`**Description:** ${cmd.description}`); + } + } + } + + try { + return message.reply(data, { split: true }); + } catch (error) { + this.dependencies.loggerService.log('error', `Could not send help DM to ${message.author.tag}.\n`, error); + + return message.reply("it seems like I can't DM you! Do you have DMs disabled?"); + } + } +} diff --git a/src/bot/commands/help.ts b/src/bot/commands/help.ts deleted file mode 100644 index 1bbd521..0000000 --- a/src/bot/commands/help.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Discord, { Collection } from 'discord.js'; -import ICommand from './ICommand'; - -import ConfigService from '../../core/services/config.service'; -import LoggerService from '../../core/services/logger.service'; - -import container from '../../inversity.config'; - -const tmpPrefix = container.resolve(ConfigService).get('BOT_TRIGGER'); -const loggerService = container.resolve(LoggerService); - -const command: ICommand = { - name: 'help', - aliases: ['commands'], - example: `\`${tmpPrefix}help ping\``, - description: 'Lists available commands!', - async execute(message: Discord.Message, args: string[], prefix: string, commands: Collection) { - const data = []; - - if (!args || args.length === 0) { - // Get for all commands - data.push("here's a list of all my commands:\n"); - - const cmds = commands.map((c) => c.name); - cmds.forEach((element) => { - const cmd = commands.get(element) || commands.find((c) => c.aliases && c.aliases.includes(element)); - let response = `\`${prefix}${cmd.name}\` `; - if (cmd.description) { - response += `**${cmd.description}** `; - } - if (cmd.aliases) { - response += `\n\t\t\t*alternatively:* \`${prefix}${cmd.aliases.join(`\`, \`${prefix}`)}\``; - } - data.push(response); - data.push(cmd.example ? `\t\t\t*for example:* ${cmd.example}\n` : ''); - }); - data.push(`\nYou can send \`${prefix}help [command name]\` to get info on a specific command!`); - } else { - // Get description of single command - const name = args[0].toLowerCase(); - const cmd = commands.get(name) || commands.find((c) => c.aliases && c.aliases.includes(name)); - - if (!cmd) { - message.reply("that's not a valid command!"); - } else { - data.push(`**Name:** ${cmd.name}`); - - if (cmd.aliases) { - data.push(`**Aliases:** ${cmd.aliases.join(', ')}`); - } - - if (cmd.description) { - data.push(`**Description:** ${cmd.description}`); - } - } - } - - try { - return message.reply(data, { split: true }); - } catch (error) { - loggerService.log('error', `Could not send help DM to ${message.author.tag}.\n`, error); - - return message.reply("it seems like I can't DM you! Do you have DMs disabled?"); - } - }, -}; - -export default command; diff --git a/src/bot/commands/index.ts b/src/bot/commands/index.ts deleted file mode 100644 index 2e6691c..0000000 --- a/src/bot/commands/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import magicball from './8ball'; -import addgreeting from './addgreeting'; -import help from './help'; -import ping from './ping'; -import quotes from './quotes'; -import see from './see'; -import character from './character'; - -export default [help, ping, see, magicball, addgreeting, character, quotes]; diff --git a/src/bot/commands/ping.command.ts b/src/bot/commands/ping.command.ts new file mode 100644 index 0000000..8abcedf --- /dev/null +++ b/src/bot/commands/ping.command.ts @@ -0,0 +1,8 @@ +import Discord from 'discord.js'; +import Command from './Command'; + +export default class PingCommand extends Command { + async execute(message: Discord.Message): Promise { + return message.reply('Pong!'); + } +} diff --git a/src/bot/commands/ping.ts b/src/bot/commands/ping.ts deleted file mode 100644 index 5a3d54f..0000000 --- a/src/bot/commands/ping.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Discord from 'discord.js'; -import ICommand from './ICommand'; - -const command: ICommand = { - name: 'ping', - aliases: ['hello', 'hi'], - description: 'Responds, kind of like telling you the bot is alive.', - async execute(message: Discord.Message) { - return message.reply('Pong!'); - }, -}; - -export default command; diff --git a/src/bot/commands/quotes.ts b/src/bot/commands/quotes.command.ts similarity index 71% rename from src/bot/commands/quotes.ts rename to src/bot/commands/quotes.command.ts index d230e02..1d9bab0 100644 --- a/src/bot/commands/quotes.ts +++ b/src/bot/commands/quotes.command.ts @@ -1,32 +1,17 @@ import Discord from 'discord.js'; import shortid from 'shortid'; + import MongoService from '../../core/services/mongo.service'; -import ConfigService from '../../core/services/config.service'; -import container from '../../inversity.config'; -import ICommand from './ICommand'; -interface Quote { - author: string; - quote: string; - shortId: string; - meta: { - authorCachedName: string; - createdBy: string; - createdByCachedName: string; - createdAt: Date; - }; -} +import Quote from '../../core/types/Quote'; -const prefix = container.resolve(ConfigService).get('BOT_TRIGGER'); +import Command from './Command'; -const command: ICommand = { - name: 'quotes', - aliases: ['quote', 'quotes', 'q'], - description: `Get random quotes from people in chat, or add quotes to the list.\n- \`${prefix}quote @author quote\` or \`${prefix}quote add @author quote\` - Adds "quote" by @author. @author can be Discord mention or any string that terminates with ' ' (space).\n- \`${prefix}quote search key words\` - Search "key words" and give you results of that quote.\n- \`${prefix}quote\` (with no arguments) - gets a random quote.\n- \`${prefix}quote #quoteId\` - Gets specific quote with ID #quoteId`, - async execute(message, args, _prefix, _commands, db) { +export default class QuotesCommand extends Command { + async execute(message: Discord.Message, args: string[]): Promise { // Get random quote if (args.filter((s) => s.trim().length).length === 0) { - getRandomQuote(message, args, db); + getRandomQuote(message, args, this.dependencies.mongoService); return; } @@ -35,28 +20,28 @@ const command: ICommand = { switch (first) { // Search quotes case 'search': - searchQuotes(message, args.slice(1), db); + searchQuotes(message, args.slice(1), this.dependencies.mongoService); return; // Add quote case 'add': default: if (first.startsWith('#')) { - getSingleQuote(message, args, db); + getSingleQuote(message, args, this.dependencies.mongoService); } else { - addNewQuote(message, args.slice(first === 'add' ? 1 : 0), db); + addNewQuote(message, args.slice(first === 'add' ? 1 : 0), this.dependencies.mongoService); } } - }, -}; + } +} const clean = (str: string): string => str.replace(/[\t\n|]+/g, ' ').replace(/\s+/g, ' '); const getQuoteStr = ({ author, quote, shortId }: Quote): string => `"${quote}" - ${author} (#${shortId})`; -async function getRandomQuote(message: Discord.Message, args: string[], db: MongoService): Promise { - const count = await db.count(message.author.id, 'quotes', {}); +async function getRandomQuote(message: Discord.Message, args: string[], mongoService: MongoService): Promise { + const count = await mongoService.count(message.author.id, 'quotes', {}); const r = Math.floor(Math.random() * count); - const q = await db.dbInstance.collection('quotes').find().skip(r).limit(1).toArray(); + const q = await mongoService.dbInstance.collection('quotes').find().skip(r).limit(1).toArray(); if (q.length > 0) { const quote: Quote = q[0]; @@ -66,8 +51,8 @@ async function getRandomQuote(message: Discord.Message, args: string[], db: Mong } } -async function searchQuotes(message: Discord.Message, args: string[], db: MongoService): Promise { - const q = await db.find(message.author.id, 'quotes', { +async function searchQuotes(message: Discord.Message, args: string[], mongoService: MongoService): Promise { + const q = await mongoService.find(message.author.id, 'quotes', { quote: { $regex: `${args.join(' ')}`, $options: 'i', @@ -86,7 +71,7 @@ async function searchQuotes(message: Discord.Message, args: string[], db: MongoS } } -async function addNewQuote(message: Discord.Message, args: string[], db: MongoService): Promise { +async function addNewQuote(message: Discord.Message, args: string[], mongoService: MongoService): Promise { const [authorRaw, ...restRaw] = args; const hasAuthor = /<@!\d+>/.test(authorRaw) || authorRaw.startsWith('@'); const author = hasAuthor ? (authorRaw.startsWith('@') ? authorRaw.slice(1) : authorRaw) : 'Anonymous'; @@ -135,13 +120,13 @@ async function addNewQuote(message: Discord.Message, args: string[], db: MongoSe }; const quoteStr = getQuoteStr(quoteObj); - db.insert(message.author.id, 'quotes', [quoteObj]); + mongoService.insert(message.author.id, 'quotes', [quoteObj]); message.reply(`${replies[Math.floor(Math.random() * replies.length)]}\n${quoteStr}`); } -async function getSingleQuote(message: Discord.Message, args: string[], db: MongoService): Promise { +async function getSingleQuote(message: Discord.Message, args: string[], mongoService: MongoService): Promise { const id = args[0].slice(1); - const quote = await db.findOne(message.author.id, 'quotes', { shortId: id }); + const quote = await mongoService.findOne(message.author.id, 'quotes', { shortId: id }); if (!quote) { message.reply("I'm sorry, I couldn't find a quote with that id!"); @@ -150,5 +135,3 @@ async function getSingleQuote(message: Discord.Message, args: string[], db: Mong message.reply(getQuoteStr(quote)); } - -export default command; diff --git a/src/bot/commands/see.command.ts b/src/bot/commands/see.command.ts new file mode 100644 index 0000000..e909daa --- /dev/null +++ b/src/bot/commands/see.command.ts @@ -0,0 +1,10 @@ +import Discord from 'discord.js'; +import Command from './Command'; + +export default class SeeCommand extends Command { + async execute(message: Discord.Message): Promise { + return message.author.send( + `Server: ${message.guild.name}\nYour username: ${message.author.username}\nYour ID: ${message.author.id}`, + ); + } +} diff --git a/src/bot/commands/see.ts b/src/bot/commands/see.ts deleted file mode 100644 index 19cddfc..0000000 --- a/src/bot/commands/see.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Discord from 'discord.js'; - -import ConfigService from '../../core/services/config.service'; -import container from '../../inversity.config'; - -import ICommand from './ICommand'; - -const prefix = container.resolve(ConfigService).get('BOT_TRIGGER'); - -const command: ICommand = { - name: 'see', - example: `\`${prefix}see\``, - description: 'Sends a DM telling you information about your user on given server.', - async execute(message: Discord.Message) { - return message.author.send( - `Server: ${message.guild.name}\nYour username: ${message.author.username}\nYour ID: ${message.author.id}`, - ); - }, -}; - -export default command; diff --git a/src/core/services/config.service.ts b/src/core/services/config.service.ts index 1656eaf..8187ffb 100644 --- a/src/core/services/config.service.ts +++ b/src/core/services/config.service.ts @@ -1,10 +1,7 @@ -import { injectable } from 'inversify'; - import Config from '../types/Config'; import Environment from '../types/Environment'; import LogLevel from '../types/LogLevel'; -@injectable() export default class ConfigService { /** * Object that holds all config values used by the system diff --git a/src/core/services/database.service.ts b/src/core/services/database.service.ts index cec3f72..877bd75 100644 --- a/src/core/services/database.service.ts +++ b/src/core/services/database.service.ts @@ -1,35 +1,34 @@ -import { injectable } from 'inversify'; import path from 'path'; import { createConnection, Connection, EntityManager } from 'typeorm'; -// eslint-disable-next-line import/no-cycle -import container from '../../inversity.config'; - import ConfigService from './config.service'; -// eslint-disable-next-line import/no-cycle import LoggerService from './logger.service'; -@injectable() export default class DatabaseService { - private _configService: ConfigService = container.resolve(ConfigService); - - private _loggerService: LoggerService = container.resolve(LoggerService); - public _connection: Connection; public get manager(): EntityManager { return this._connection.manager; } - public async connect(): Promise { - this._connection = await createConnection({ - type: 'postgres', - url: this._configService.get('DATABASE_URL'), - entities: [path.resolve(__dirname, '../../**/*.entity{.ts,.js}')], - synchronize: true, - }); + constructor(private _configService: ConfigService, private _loggerService: LoggerService) {} - this._loggerService.log('info', 'Venom is connected to Postgres'); + async connect(): Promise { + try { + this._connection = await createConnection({ + type: 'postgres', + url: this._configService.get('DATABASE_URL'), + entities: [path.resolve(__dirname, '../../**/*.entity{.ts,.js}')], + synchronize: true, + }); + this._loggerService.log('info', 'Venom is connected to Postgres'); + + return true; + } catch (error) { + this._loggerService.log('error', 'Venom could not connect to Postgres', { error }); + + return false; + } } public disconnect(): void { diff --git a/src/core/services/http.service.ts b/src/core/services/http.service.ts index f5d138e..3515ee6 100644 --- a/src/core/services/http.service.ts +++ b/src/core/services/http.service.ts @@ -1,15 +1,20 @@ -import { injectable } from 'inversify'; import Axios, { AxiosStatic, AxiosRequestConfig, AxiosResponse } from 'axios'; -@injectable() +import LoggerService from './logger.service'; + export default class HttpService { private _client: AxiosStatic; - constructor() { + constructor(private _loggerService: LoggerService) { this._client = Axios; } public async get(url: string, config?: AxiosRequestConfig): Promise { + this._loggerService.log('debug', 'Sending HTTP GET', { + url, + config, + }); + return this._client.get(url, config); } } diff --git a/src/core/services/logger.service.ts b/src/core/services/logger.service.ts index 1f179c9..0459667 100644 --- a/src/core/services/logger.service.ts +++ b/src/core/services/logger.service.ts @@ -1,20 +1,14 @@ -import { injectable } from 'inversify'; import path from 'path'; import winston from 'winston'; -// eslint-disable-next-line import/no-cycle -import container from '../../inversity.config'; import LogLevel from '../types/LogLevel'; import ConfigService from './config.service'; -@injectable() export default class LoggerService { - private _configService: ConfigService = container.resolve(ConfigService); - private _logger: winston.Logger; - constructor() { + constructor(private _configService: ConfigService) { this._logger = winston.createLogger({ level: this._configService.get('LOG_LEVEL'), format: winston.format.json(), diff --git a/src/core/services/mongo.service.ts b/src/core/services/mongo.service.ts index 4f57849..8abdd4d 100644 --- a/src/core/services/mongo.service.ts +++ b/src/core/services/mongo.service.ts @@ -1,42 +1,44 @@ -import { injectable } from 'inversify'; import mongodb, { FilterQuery } from 'mongodb'; import ConfigService from './config.service'; -// eslint-disable-next-line import/no-cycle import LoggerService from './logger.service'; -// eslint-disable-next-line import/no-cycle -import container from '../../inversity.config'; -@injectable() export default class MongoService { - private _configService: ConfigService = container.resolve(ConfigService); - - private _loggerService: LoggerService = container.resolve(LoggerService); - private _mongoClient: mongodb.MongoClient; public _db: mongodb.Db; + constructor(private _configService: ConfigService, private _loggerService: LoggerService) {} + public get dbInstance(): mongodb.Db { return this._db; } - public async connect(): Promise { - return new Promise((resolve, reject) => { + public async connect(): Promise { + const prom = new Promise((resolve, reject) => { this._mongoClient = new mongodb.MongoClient(this._configService.get('MONGODB_URI'), { useUnifiedTopology: true }); - this._mongoClient.connect((err) => { - if (err) { - this._loggerService.log('error', 'Venom could not connect to MongoDB', err); - reject(); + this._mongoClient.connect((error) => { + if (error) { + reject(error); } else { - this._loggerService.log('info', 'Venom is connected to MongoDB'); - this._db = this._mongoClient.db(this._configService.get('MONGODB_DB_NAME')); resolve(); } }); }); + + try { + await prom; + + this._loggerService.log('info', 'Venom is connected to MongoDB'); + + return true; + } catch (error) { + this._loggerService.log('error', 'Venom could not connect to MongoDB', error); + + return false; + } } public disconnect(): void { diff --git a/src/core/types/Dependencies.ts b/src/core/types/Dependencies.ts new file mode 100644 index 0000000..781766c --- /dev/null +++ b/src/core/types/Dependencies.ts @@ -0,0 +1,13 @@ +import ConfigService from '../services/config.service'; +import DatabaseService from '../services/database.service'; +import HttpService from '../services/http.service'; +import LoggerService from '../services/logger.service'; +import MongoService from '../services/mongo.service'; + +export default interface Dependencies { + configService: ConfigService; + databaseService: DatabaseService; + httpService: HttpService; + loggerService: LoggerService; + mongoService: MongoService; +} diff --git a/src/core/types/Quote.ts b/src/core/types/Quote.ts new file mode 100644 index 0000000..66038a1 --- /dev/null +++ b/src/core/types/Quote.ts @@ -0,0 +1,11 @@ +export default interface Quote { + author: string; + quote: string; + shortId: string; + meta: { + authorCachedName: string; + createdBy: string; + createdByCachedName: string; + createdAt: Date; + }; +} diff --git a/src/inversity.config.ts b/src/inversity.config.ts deleted file mode 100644 index 59e2746..0000000 --- a/src/inversity.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import 'reflect-metadata'; -import { Container } from 'inversify'; - -import ConfigService from './core/services/config.service'; -import HttpService from './core/services/http.service'; -// eslint-disable-next-line import/no-cycle -import LoggerService from './core/services/logger.service'; -// eslint-disable-next-line import/no-cycle -import MongoService from './core/services/mongo.service'; -// eslint-disable-next-line import/no-cycle -import DatabaseService from './core/services/database.service'; - -const container = new Container(); -container.bind(ConfigService).toSelf(); -container.bind(HttpService).toSelf(); -container.bind(LoggerService).toSelf(); -container.bind(MongoService).toSelf(); -container.bind(DatabaseService).toSelf(); - -export default container; diff --git a/src/main.ts b/src/main.ts index 10790dd..54f6852 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,9 @@ +import 'reflect-metadata'; + import dotenv from 'dotenv'; import { exit } from 'process'; import path from 'path'; -import 'reflect-metadata'; - import App from './app'; // Load config @@ -17,21 +17,14 @@ function exitHandler(): void { exit(); } -try { - app.init(); +// do something when app is closing +process.on('exit', exitHandler); +// catches ctrl+c event +process.on('SIGINT', exitHandler); +// catches "kill pid" (for example: nodemon restart) +process.on('SIGUSR1', exitHandler); +process.on('SIGUSR2', exitHandler); +// catches uncaught exceptions +process.on('uncaughtException', exitHandler); - // do something when app is closing - process.on('exit', exitHandler); - - // catches ctrl+c event - process.on('SIGINT', exitHandler); - - // catches "kill pid" (for example: nodemon restart) - process.on('SIGUSR1', exitHandler); - process.on('SIGUSR2', exitHandler); - - // catches uncaught exceptions - process.on('uncaughtException', exitHandler); -} catch { - exit(1); -} +app.start(); diff --git a/src/tools/quotes/collect.ts b/src/tools/quotes/collect.ts index 8ceb1c3..2cebb8a 100644 --- a/src/tools/quotes/collect.ts +++ b/src/tools/quotes/collect.ts @@ -13,6 +13,8 @@ import path from 'path'; import HttpService from 'src/core/services/http.service'; import cheerio from 'cheerio'; import { Quote } from './IQuote'; +import LoggerService from '../../core/services/logger.service'; +import ConfigService from '../../core/services/config.service'; const PAGE_SIZE = 15; const MAX_PAGE = 35; @@ -31,7 +33,9 @@ async function run(): Promise { }); const promises: Array> = []; - const http = new HttpService(); + const configService = new ConfigService(); + const loggerService = new LoggerService(configService); + const http = new HttpService(loggerService); for (let i = 0; i < PAGE_SIZE * MAX_PAGE; i += PAGE_SIZE) { const urlWithPage = `${url}&st=${i}`; diff --git a/yarn.lock b/yarn.lock index f12560a..a19cdcf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -519,6 +519,11 @@ dependencies: reflect-metadata "*" +"@types/shortid@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/shortid/-/shortid-0.0.29.tgz#8093ee0416a6e2bf2aa6338109114b3fbffa0e9b" + integrity sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps= + "@types/sinon@^9.0.4": version "9.0.4" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" @@ -2942,11 +2947,6 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -inversify@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.0.1.tgz#500d709b1434896ce5a0d58915c4a4210e34fb6e" - integrity sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ== - irregular-plurals@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.2.0.tgz#b19c490a0723798db51b235d7e39add44dab0822"