Skip to content

Commit

Permalink
Refactor storage use to support rewrite
Browse files Browse the repository at this point in the history
Still a lot of cleanup to do, and I need to write another abstraction class on top of StorageProvider for client storage, forgot I was going to need to do that.
  • Loading branch information
zajrik committed Mar 27, 2017
1 parent 13a7765 commit 63292eb
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 86 deletions.
93 changes: 51 additions & 42 deletions src/lib/bot/Bot.ts
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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<string, GuildStorage>;
public storage: ClientStorage;
public commands: CommandRegistry<this, string, Command<this>>;

private _token: string;
private _guildDataStorage: LocalStorage;
private _guildSettingStorage: LocalStorage;
private _guildDataStorage: StorageProvider;
private _guildSettingStorage: StorageProvider;
private _guildStorageLoader: GuildStorageLoader<this>;
private _commandLoader: CommandLoader<this>;
private _dispatcher: CommandDispatcher<this>;
Expand Down Expand Up @@ -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 = <typeof StorageProvider> (botOptions.provider || JSONProvider);

this._guildDataStorage = new (<any> this.provider)('guild_storage');
this._guildSettingStorage = new (<any> 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<string, GuildStorage>}
* @name guildStorages
* @instance
*/
this.guildStorages = new GuildStorageRegistry();
this.storage = new (<any> this.provider)('client_storage');
applyClientStorageMixin(this.storage);

/**
* [Collection]{@link external:Collection} containing all loaded commands
Expand All @@ -190,11 +185,6 @@ export class Bot extends Client
*/
this.commands = new CommandRegistry<this, string, Command<this>>();

// 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>(this) : null;

Expand All @@ -209,6 +199,18 @@ export class Bot extends Client
if (!this.passive) this.loadCommand('all');
}

private async init(): Promise<void>
{
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
Expand Down Expand Up @@ -244,25 +246,28 @@ 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);
});

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;
Expand All @@ -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>
{
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;
}
Expand All @@ -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>
{
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<boolean>
{
return !!this.storage.getItem('defaultGuildSettings')[key];
return typeof await this.storage.get(`defaultGuildSettings.${key}`) !== 'undefined';
}

/**
Expand All @@ -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<string>
{
if (!guild) return null;
return this.guildStorages.get(<Guild> guild).getSetting('prefix') || null;
return (await this.storage.guilds.get(guild.id).settings.get('prefix')) || null;
}

/**
Expand Down Expand Up @@ -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
Expand Down
42 changes: 21 additions & 21 deletions src/lib/command/CommandDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,25 +34,25 @@ export class CommandDispatcher<T extends Bot>
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<T>, string, string] = this.isCommandCalled(message);
const [commandCalled, command, prefix, name]: [boolean, Command<T>, string, string] = await this.isCommandCalled(message);
if (!commandCalled) return;
if (command.ownerOnly && !this._bot.isOwner(message.author)) return;

// Check ratelimits
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;

Expand Down Expand Up @@ -99,15 +99,15 @@ export class CommandDispatcher<T extends Bot>
* 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<T>, string, string]
private async isCommandCalled(message: Message): Promise<[boolean, Command<T>, string, string]>
{
const dm: boolean = message.channel.type !== 'text';
const prefixes: string[] = [
`<@${this._bot.user.id}>`,
`<@!${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));

Expand All @@ -129,13 +129,13 @@ export class CommandDispatcher<T extends Bot>
* Test if the command caller is allowed to use the command
* under whatever circumstances are present at call-time
*/
private testCommand(command: Command<T>, message: Message): boolean
private async testCommand(command: Command<T>, message: Message): Promise<boolean>
{
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);
Expand Down Expand Up @@ -205,11 +205,11 @@ export class CommandDispatcher<T extends Bot>
/**
* Compare user roles to the command's limiter
*/
private checkLimiter(command: Command<T>, message: Message, dm: boolean): boolean
private async checkLimiter(command: Command<T>, message: Message, dm: boolean): Promise<boolean>
{
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 =>
Expand All @@ -229,10 +229,10 @@ export class CommandDispatcher<T extends Bot>
/**
* 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<boolean>
{
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;
}

Expand Down Expand Up @@ -279,10 +279,10 @@ export class CommandDispatcher<T extends Bot>
/**
* Return an error for failing a command limiter
*/
private failedLimiterError(command: Command<T>, message: Message): string
private async failedLimiterError(command: Command<T>, message: Message): Promise<string>
{
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'}`
Expand Down
36 changes: 21 additions & 15 deletions src/lib/storage/GuildStorageLoader.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,39 +20,45 @@ export class GuildStorageLoader<T extends Bot>
* 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<void>
{
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);
}

/**
* Assign a GuildStorage to guilds that lack one due to the bot being
* 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<void>
{
let storagelessGuilds: Collection<string, Guild> = this._bot.guilds.filter(guild => !dataStorage.keys.includes(guild.id));
const dataStorageKeys: string[] = await dataStorage.keys();
let storagelessGuilds: Collection<string, Guild> = 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<void>
{
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);
}
}
}
Loading

0 comments on commit 63292eb

Please sign in to comment.