diff --git a/src/lib/bot/Bot.ts b/src/lib/bot/Bot.ts index 3d19fb6a..78448f97 100644 --- a/src/lib/bot/Bot.ts +++ b/src/lib/bot/Bot.ts @@ -1,15 +1,16 @@ import { Client, ClientOptions, Guild, Message, Channel, Emoji, User, GuildMember, Collection, MessageReaction, Role, UserResolvable } from 'discord.js'; import { BotOptions } from '../types/BotOptions'; -import { LocalStorage } from '../storage/LocalStorage'; -import { GuildStorage } from '../storage/GuildStorage'; import { GuildStorageLoader } from '../storage/GuildStorageLoader'; -import { GuildStorageRegistry } from '../storage/GuildStorageRegistry'; import { Command } from '../command/Command'; import { CommandLoader } from '../command/CommandLoader'; import { CommandRegistry } from '../command/CommandRegistry'; import { CommandDispatcher } from '../command/CommandDispatcher'; import { RateLimiter } from '../command/RateLimiter'; import { MiddlewareFunction } from '../types/MiddlewareFunction'; +import { StorageProvider } from '../storage/StorageProvider'; +import { JSONProvider } from '../storage/JSONProvider'; +import { ClientStorage } from '../types/ClientStorage'; +import { applyClientStorageMixin } from '../storage/mixin/ClientStorageMixin'; /** * The Discord.js Client instance. Contains bot-specific [storage]{@link Bot#storage}, @@ -32,16 +33,16 @@ export class Bot extends Client public version: string; public disableBase: string[]; public config: any; + public provider: typeof StorageProvider; public _middleware: MiddlewareFunction[]; public _rateLimiter: RateLimiter; - public storage: LocalStorage; - public guildStorages: GuildStorageRegistry; + public storage: ClientStorage; public commands: CommandRegistry>; private _token: string; - private _guildDataStorage: LocalStorage; - private _guildSettingStorage: LocalStorage; + private _guildDataStorage: StorageProvider; + private _guildSettingStorage: StorageProvider; private _guildStorageLoader: GuildStorageLoader; private _commandLoader: CommandLoader; private _dispatcher: CommandDispatcher; @@ -159,27 +160,21 @@ export class Bot extends Client // Middleware function storage for the bot instance this._middleware = []; - this._guildDataStorage = new LocalStorage('storage/guild-storage'); - this._guildSettingStorage = new LocalStorage('storage/guild-settings'); + this.provider = (botOptions.provider || JSONProvider); + + this._guildDataStorage = new ( this.provider)('guild_storage'); + this._guildSettingStorage = new ( this.provider)('guild_settings'); this._guildStorageLoader = new GuildStorageLoader(this); /** * Bot-specific storage * @memberof Bot - * @type {LocalStorage} + * @type {StorageProvider} * @name storage * @instance */ - this.storage = new LocalStorage('storage/bot-storage'); - - /** - * [Collection]{@link external:Collection} containing all GuildStorage instances - * @memberof Bot - * @type {GuildStorageRegistry} - * @name guildStorages - * @instance - */ - this.guildStorages = new GuildStorageRegistry(); + this.storage = new ( this.provider)('client_storage'); + applyClientStorageMixin(this.storage); /** * [Collection]{@link external:Collection} containing all loaded commands @@ -190,11 +185,6 @@ export class Bot extends Client */ this.commands = new CommandRegistry>(); - // Load defaultGuildSettings into storage the first time the bot is run - if (!this.storage.exists('defaultGuildSettings')) - this.storage.setItem('defaultGuildSettings', - require('../storage/defaultGuildSettings.json')); - this._commandLoader = !this.passive ? new CommandLoader(this) : null; this._dispatcher = !this.passive ? new CommandDispatcher(this) : null; @@ -209,6 +199,18 @@ export class Bot extends Client if (!this.passive) this.loadCommand('all'); } + private async init(): Promise + { + await this.storage.init(); + await this._guildDataStorage.init(); + await this._guildSettingStorage.init(); + + // Load defaultGuildSettings into storage the first time the bot is run + if (typeof await this.storage.get('defaultGuildSettings') === 'undefined') + await this.storage.set('defaultGuildSettings', + require('../storage/defaultGuildSettings.json')); + } + /** * Returns whether or not the given user is an owner * of the bot @@ -244,15 +246,18 @@ export class Bot extends Client { this.login(this._token); - this.once('ready', () => + this.once('ready', async () => { - console.log(this.readyText); + await this.init(); this.user.setGame(this.statusText); // Load all guild storages - this._guildStorageLoader.loadStorages(this._guildDataStorage, this._guildSettingStorage); + await this._guildStorageLoader.loadStorages(this._guildDataStorage, this._guildSettingStorage); + this.emit('waiting'); }); + this.once('finished', () => this.emit('clientReady')); + this.on('guildCreate', () => { this._guildStorageLoader.initNewGuilds(this._guildDataStorage, this._guildSettingStorage); @@ -260,9 +265,9 @@ export class Bot extends Client this.on('guildDelete', (guild) => { - this.guildStorages.delete(guild.id); - this._guildDataStorage.removeItem(guild.id); - this._guildSettingStorage.removeItem(guild.id); + this.storage.guilds.delete(guild.id); + this._guildDataStorage.remove(guild.id); + this._guildSettingStorage.remove(guild.id); }); return this; @@ -278,11 +283,12 @@ export class Bot extends Client * @param {any} value - The value to use in settings storage * @returns {Bot} */ - public setDefaultSetting(key: string, value: any): this + public async setDefaultSetting(key: string, value: any): Promise { - this.storage.setItem(`defaultGuildSettings/${key}`, value); - for (const guild of this.guildStorages.values()) - if (!guild.settingExists(key)) guild.setSetting(key, value); + await this.storage.set(`defaultGuildSettings/${key}`, value); + for (const guildStorage of this.storage.guilds.values()) + if (typeof await guildStorage.settings.get(key) === 'undefined') + await guildStorage.settings.set(key, value); return this; } @@ -296,22 +302,22 @@ export class Bot extends Client * @param {string} key - The key to use in settings storage * @returns {Bot} */ - public removeDefaultSetting(key: string): this + public async removeDefaultSetting(key: string): Promise { - this.storage.removeItem(`defaultGuildSettings/${key}`); + await this.storage.remove(`defaultGuildSettings.${key}`); return this; } /** - * See if a guild default setting exists + * See if a default guild setting exists * @memberof Bot * @instance * @param {string} key - The key in storage to check * @returns {boolean} */ - public defaultSettingExists(key: string): boolean + public async defaultSettingExists(key: string): Promise { - return !!this.storage.getItem('defaultGuildSettings')[key]; + return typeof await this.storage.get(`defaultGuildSettings.${key}`) !== 'undefined'; } /** @@ -321,10 +327,10 @@ export class Bot extends Client * @param {(external:Guild|string)} guild The guild or guild id to get the prefix of * @returns {string|null} */ - public getPrefix(guild: Guild | string): string + public async getPrefix(guild: Guild): Promise { if (!guild) return null; - return this.guildStorages.get( guild).getSetting('prefix') || null; + return (await this.storage.guilds.get(guild.id).settings.get('prefix')) || null; } /** @@ -423,6 +429,9 @@ export class Bot extends Client public on(event: 'command', listener: (name: string, args: any[], execTime: number, message: Message) => void): this; public on(event: 'blacklistAdd', listener: (user: User, global: boolean) => void): this; public on(event: 'blacklistRemove', listener: (user: User, global: boolean) => void): this; + public on(event: 'waiting', listener: () => void): this; + public on(event: 'finished', listener: () => void): this; + public on(event: 'clientReady', listener: () => void): this; /** * Emitted whenever a command is successfully called diff --git a/src/lib/command/CommandDispatcher.ts b/src/lib/command/CommandDispatcher.ts index acbcafc7..fb89ea05 100644 --- a/src/lib/command/CommandDispatcher.ts +++ b/src/lib/command/CommandDispatcher.ts @@ -2,7 +2,7 @@ import { RateLimiter } from './RateLimiter'; import { PermissionResolvable, TextChannel, User } from 'discord.js'; import { MiddlewareFunction } from '../types/MiddlewareFunction'; import { Message } from '../types/Message'; -import { GuildStorage } from '../storage/GuildStorage'; +import { GuildStorage } from '../types/GuildStorage'; import { Command } from '../command/Command'; import { Bot } from '../bot/Bot'; import { RateLimit } from './RateLimit'; @@ -34,12 +34,12 @@ export class CommandDispatcher if (message.author.bot) return; const dm: boolean = message.channel.type !== 'text'; - if (!dm) message.guild.storage = this._bot.guildStorages.get(message.guild); + if (!dm) message.guild.storage = this._bot.storage.guilds.get(message.guild.id); // Check blacklist - if (this.isBlacklisted(message.author, message, dm)) return; + if (await this.isBlacklisted(message.author, message, dm)) return; - const [commandCalled, command, prefix, name]: [boolean, Command, string, string] = this.isCommandCalled(message); + const [commandCalled, command, prefix, name]: [boolean, Command, string, string] = await this.isCommandCalled(message); if (!commandCalled) return; if (command.ownerOnly && !this._bot.isOwner(message.author)) return; @@ -47,12 +47,12 @@ export class CommandDispatcher if (!this.checkRateLimits(message, command)) return; // Remove bot from message.mentions if only mentioned one time as a prefix - if (!(!dm && prefix === message.guild.storage.getSetting('prefix')) && prefix !== '' + if (!(!dm && prefix === await message.guild.storage.settings.get('prefix')) && prefix !== '' && (message.content.match(new RegExp(`<@!?${this._bot.user.id}>`, 'g')) || []).length === 1) message.mentions.users.delete(this._bot.user.id); let validCaller: boolean = false; - try { validCaller = this.testCommand(command, message); } + try { validCaller = await this.testCommand(command, message); } catch (err) { message[this._bot.selfbot ? 'channel' : 'author'].send(err); } if (!validCaller) return; @@ -99,7 +99,7 @@ export class CommandDispatcher * the prefix used to call the command, and the name or alias * of the command used to call it */ - private isCommandCalled(message: Message): [boolean, Command, string, string] + private async isCommandCalled(message: Message): Promise<[boolean, Command, string, string]> { const dm: boolean = message.channel.type !== 'text'; const prefixes: string[] = [ @@ -107,7 +107,7 @@ export class CommandDispatcher `<@!${this._bot.user.id}>` ]; - if (!dm) prefixes.push(message.guild.storage.getSetting('prefix')); + if (!dm) prefixes.push(await message.guild.storage.settings.get('prefix')); let prefix: string = prefixes.find(a => message.content.trim().startsWith(a)); @@ -129,13 +129,13 @@ export class CommandDispatcher * Test if the command caller is allowed to use the command * under whatever circumstances are present at call-time */ - private testCommand(command: Command, message: Message): boolean + private async testCommand(command: Command, message: Message): Promise { const dm: boolean = message.channel.type !== 'text'; - const storage: GuildStorage = !dm ? this._bot.guildStorages.get(message.guild) : null; + const storage: GuildStorage = !dm ? this._bot.storage.guilds.get(message.guild.id) : null; - if (!dm && storage.settingExists('disabledGroups') - && storage.getSetting('disabledGroups').includes(command.group)) return false; + if (!dm && typeof await storage.settings.get('disabledGroups') !== 'undefined' + && (await storage.settings.get('disabledGroups')).includes(command.group)) return false; if (dm && command.guildOnly) throw this.guildOnlyError(); let missingPermissions: PermissionResolvable[] = this.checkPermissions(command, message, dm); @@ -205,11 +205,11 @@ export class CommandDispatcher /** * Compare user roles to the command's limiter */ - private checkLimiter(command: Command, message: Message, dm: boolean): boolean + private async checkLimiter(command: Command, message: Message, dm: boolean): Promise { if (dm || this._bot.selfbot) return true; - let storage: GuildStorage = this._bot.guildStorages.get(message.guild); - let limitedCommands: { [name: string]: string[] } = storage.getSetting('limitedCommands') || {}; + let storage: GuildStorage = this._bot.storage.guilds.get(message.guild.id); + let limitedCommands: { [name: string]: string[] } = await storage.settings.get('limitedCommands') || {}; if (!limitedCommands[command.name]) return true; if (limitedCommands[command.name].length === 0) return true; return message.member.roles.filter(role => @@ -229,10 +229,10 @@ export class CommandDispatcher /** * Check if the calling user is blacklisted */ - private isBlacklisted(user: User, message: Message, dm: boolean): boolean + private async isBlacklisted(user: User, message: Message, dm: boolean): Promise { - if (this._bot.storage.exists(`blacklist/${user.id}`)) return true; - if (!dm && message.guild.storage.settingExists(`blacklist/${user.id}`)) return true; + if (await this._bot.storage.get(`blacklist.${user.id}`)) return true; + if (!dm && message.guild.storage.settings.get(`blacklist.${user.id}`)) return true; return false; } @@ -279,10 +279,10 @@ export class CommandDispatcher /** * Return an error for failing a command limiter */ - private failedLimiterError(command: Command, message: Message): string + private async failedLimiterError(command: Command, message: Message): Promise { - const storage: GuildStorage = this._bot.guildStorages.get(message.guild); - let limitedCommands: { [name: string]: string[] } = storage.getSetting('limitedCommands'); + const storage: GuildStorage = this._bot.storage.guilds.get(message.guild.id); + let limitedCommands: { [name: string]: string[] } = await storage.settings.get('limitedCommands'); let roles: string[] = limitedCommands[command.name]; return `**You must have ${roles.length > 1 ? 'one of the following roles' : 'the following role'}` diff --git a/src/lib/storage/GuildStorageLoader.ts b/src/lib/storage/GuildStorageLoader.ts index f910aecc..26e82e1d 100644 --- a/src/lib/storage/GuildStorageLoader.ts +++ b/src/lib/storage/GuildStorageLoader.ts @@ -1,7 +1,7 @@ -import { GuildStorage } from './GuildStorage'; -import { LocalStorage } from './LocalStorage'; import { Collection, Guild } from 'discord.js'; import { Bot } from '../bot/Bot'; +import { StorageProvider } from './StorageProvider'; +import { createGuildStorageMixin } from '../storage/mixin/GuildStorageMixin'; /** * Handles loading all guild-specific data from persistent storage into @@ -20,12 +20,14 @@ export class GuildStorageLoader * Load data for each guild from persistent storage and store it in a * {@link GuildStorage} object */ - public loadStorages(dataStorage: LocalStorage, settingsStorage: LocalStorage): void + public async loadStorages(dataStorage: StorageProvider, settingsStorage: StorageProvider): Promise { - for (const key of dataStorage.keys) - this._bot.guildStorages.set(key, new GuildStorage(this._bot, key, dataStorage, settingsStorage)); + for (const key of await dataStorage.keys()) + // this._bot.guildStorages.set(key, new GuildStorage(this._bot, key, dataStorage, settingsStorage)); + this._bot.storage.guilds.set(key, await createGuildStorageMixin( + dataStorage, settingsStorage, this._bot.guilds.get(key), this._bot)); - this.initNewGuilds(dataStorage, settingsStorage); + await this.initNewGuilds(dataStorage, settingsStorage); } /** @@ -33,26 +35,30 @@ export class GuildStorageLoader * in the guild before adopting this storage spec or the bot being * added to a new guild */ - public initNewGuilds(dataStorage: LocalStorage, settingsStorage: LocalStorage): void + public async initNewGuilds(dataStorage: StorageProvider, settingsStorage: StorageProvider): Promise { - let storagelessGuilds: Collection = this._bot.guilds.filter(guild => !dataStorage.keys.includes(guild.id)); + const dataStorageKeys: string[] = await dataStorage.keys(); + let storagelessGuilds: Collection = this._bot.guilds.filter(g => !dataStorageKeys.includes(g.id)); for (const guild of storagelessGuilds.values()) - this._bot.guildStorages.set(guild.id, new GuildStorage(this._bot, guild.id, dataStorage, settingsStorage)); + this._bot.storage.guilds.set(guild.id, await createGuildStorageMixin( + dataStorage, settingsStorage, guild, this._bot)); } /** * Clean out any storages/settings storages for guilds the * bot is no longer a part of */ - public cleanGuilds(dataStorage: LocalStorage, settingsStorage: LocalStorage): void + public async cleanGuilds(dataStorage: StorageProvider, settingsStorage: StorageProvider): Promise { - let guildlessStorages: string[] = dataStorage.keys.filter(guild => !this._bot.guilds.has(guild)); - let guildlessSettings: string[] = settingsStorage.keys.filter(guild => !this._bot.guilds.has(guild)); - for (const settings of guildlessSettings) settingsStorage.removeItem(settings); + const dataStorageKeys: string[] = await dataStorage.keys(); + const settingsStorageKeys: string[] = await settingsStorage.keys(); + let guildlessStorages: string[] = dataStorageKeys.filter(guild => !this._bot.guilds.has(guild)); + let guildlessSettings: string[] = settingsStorageKeys.filter(guild => !this._bot.guilds.has(guild)); + for (const settings of guildlessSettings) await settingsStorage.remove(settings); for (const storage of guildlessStorages) { - this._bot.guildStorages.delete(storage); - dataStorage.removeItem(storage); + this._bot.storage.guilds.delete(storage); + await dataStorage.remove(storage); } } } diff --git a/src/lib/storage/StorageProvider.ts b/src/lib/storage/StorageProvider.ts index e3cda74f..a8d3692b 100644 --- a/src/lib/storage/StorageProvider.ts +++ b/src/lib/storage/StorageProvider.ts @@ -1,9 +1,9 @@ -export abstract class StorageProvider +export class StorageProvider { - public abstract async init(): Promise; - public abstract async keys(): Promise; - public abstract async get(key: string): Promise; - public abstract async set(key: string, value: string): Promise; - public abstract async remove(key: string): Promise; - public abstract async clear(): Promise; + public async init(): Promise { throw new Error('Storage providers must implement the `init` method'); } + public async keys(): Promise { throw new Error('Storage providers must implement the `keys` method'); } + public async get(key: string): Promise { throw new Error('Storage providers must implement the `get` method'); } + public async set(key: string, value: string): Promise { throw new Error('Storage providers must implement the `set` method'); } + public async remove(key: string): Promise { throw new Error('Storage providers must implement the `remove` method'); } + public async clear(): Promise { throw new Error('Storage providers must implement the `clear` method'); } } diff --git a/src/lib/types/BotOptions.ts b/src/lib/types/BotOptions.ts index dffe90c0..661423de 100644 --- a/src/lib/types/BotOptions.ts +++ b/src/lib/types/BotOptions.ts @@ -1,8 +1,10 @@ import { BaseCommandName } from './BaseCommandName'; +import { StorageProvider } from '../storage/StorageProvider'; export type BotOptions = { name: string; token: string; + provider: new(name: string) => StorageProvider; commandsDir?: string; statusText?: string; readyText?: string; @@ -20,6 +22,7 @@ export type BotOptions = { * passed to a Bot on construction * @property {string} [name='botname'] See: {@link Bot#name} * @property {string} token See: {@link Bot#token} + * @property {string} provider See: {@link Bot#provider} * @property {string} [commandsDir] See: {@link Bot#commandsDir} * @property {string} [statusText=null] See: {@link Bot#statusText} * @property {string} [readyText='Ready!'] See: {@link Bot#readyText} diff --git a/src/lib/types/Guild.ts b/src/lib/types/Guild.ts index d6ccf4fc..3698917f 100644 --- a/src/lib/types/Guild.ts +++ b/src/lib/types/Guild.ts @@ -1,4 +1,4 @@ -import { GuildStorage } from '../storage/GuildStorage'; +import { GuildStorage } from './GuildStorage'; import * as Discord from 'discord.js'; export class Guild extends Discord.Guild