diff options
-rw-r--r-- | .vscode/launch.json | 6 | ||||
-rw-r--r-- | src/bot.ts | 1 | ||||
-rw-r--r-- | src/commands/dev/superUser.ts | 6 | ||||
-rw-r--r-- | src/commands/info/userInfo.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 76 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 42 | ||||
-rw-r--r-- | src/lib/index.ts | 1 | ||||
-rw-r--r-- | src/lib/models/Global.ts | 9 | ||||
-rw-r--r-- | src/lib/models/Guild.ts | 1 | ||||
-rw-r--r-- | src/lib/models/Level.ts | 1 | ||||
-rw-r--r-- | src/lib/models/Shared.ts | 49 | ||||
-rw-r--r-- | src/lib/models/__helpers.ts | 1 | ||||
-rw-r--r-- | src/lib/utils/BushCache.ts | 7 | ||||
-rw-r--r-- | src/tasks/updateCache.ts | 28 | ||||
-rw-r--r-- | src/tasks/updateSuperUsers.ts | 28 |
15 files changed, 184 insertions, 74 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index 3237d6d..91f1fa1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,12 @@ { "configurations": [ { + "command": "yarn start", + "name": "yarn start", + "request": "launch", + "type": "node-terminal" + }, + { "command": "yarn dev", "name": "BushBot", "request": "launch", @@ -9,6 +9,7 @@ const isDry = process.argv.includes('dry'); if (!isDry) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd()); BushClient.extendStructures(); const client = new BushClient(config); +if (!isDry) await client.dbPreInit(); await client.init(); if (isDry) { await client.destroy(); diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index 3a6406d..f937ad4 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -1,4 +1,4 @@ -import { BushCommand, Global, type ArgType, type BushMessage } from '#lib'; +import { BushCommand, type ArgType, type BushMessage } from '#lib'; import { type ArgumentOptions, type Flag } from 'discord-akairo'; export default class SuperUserCommand extends BushCommand { @@ -57,12 +57,12 @@ export default class SuperUserCommand extends BushCommand { if (!message.author.isOwner()) return await message.util.reply(`${util.emojis.error} Only my developers can run this command.`); - const superUsers: string[] = (await Global.findByPk(client.config.environment))?.superUsers ?? []; + const superUsers: string[] = util.getShared('superUsers'); if (action === 'add' ? superUsers.includes(user.id) : !superUsers.includes(user.id)) return message.util.reply(`${util.emojis.warn} \`${user.tag}\` is ${action === 'add' ? 'already' : 'not'} a superuser.`); - const success = await util.insertOrRemoveFromGlobal(action, 'superUsers', user.id).catch(() => false); + const success = await util.insertOrRemoveFromShared(action, 'superUsers', user.id).catch(() => false); if (success) { return await message.util.reply( diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts index f8c7c68..609bd94 100644 --- a/src/commands/info/userInfo.ts +++ b/src/commands/info/userInfo.ts @@ -54,7 +54,7 @@ export default class UserInfoCommand extends BushCommand { public static async makeUserInfoEmbed(user: BushUser, member?: BushGuildMember, guild?: BushGuild | null) { const emojis = []; - const superUsers = client.cache.global.superUsers; + const superUsers = util.getShared('superUsers'); const userEmbed: MessageEmbed = new MessageEmbed() .setTitle(util.discord.escapeMarkdown(user.tag)) diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 8cbc6fe..65b60eb 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -42,7 +42,7 @@ import { import EventEmitter from 'events'; import path from 'path'; import readline from 'readline'; -import type { Sequelize as SequelizeType } from 'sequelize'; +import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize'; import { fileURLToPath } from 'url'; import UpdateCacheTask from '../../../tasks/updateCache.js'; import UpdateStatsTask from '../../../tasks/updateStats.js'; @@ -52,6 +52,7 @@ import { Guild as GuildModel } from '../../models/Guild.js'; import { Level } from '../../models/Level.js'; import { ModLog } from '../../models/ModLog.js'; import { Reminder } from '../../models/Reminder.js'; +import { Shared } from '../../models/Shared.js'; import { Stat } from '../../models/Stat.js'; import { StickyRole } from '../../models/StickyRole.js'; import { AllowedMentions } from '../../utils/AllowedMentions.js'; @@ -152,9 +153,14 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re public contextMenuCommandHandler: ContextMenuCommandHandler; /** - * The database connection for the bot. + * The database connection for this instance of the bot (production, beta, or development). */ - public db: SequelizeType; + public instanceDB: SequelizeType; + + /** + * The database connection that is shared between all instances of the bot. + */ + public sharedDB: SequelizeType; /** * A custom logging system for the bot. @@ -201,6 +207,9 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re this.token = config.token as If<Ready, string, string | null>; this.config = config; + this.util = new BushClientUtil(this); + + /* handlers */ this.listenerHandler = new BushListenerHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'listeners'), automateCategories: true @@ -250,9 +259,9 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re directory: path.join(__dirname, '..', '..', '..', 'context-menu-commands'), automateCategories: true }); - this.util = new BushClientUtil(this); - this.db = new Sequelize({ - database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot', + + /* databases */ + const sharedDBOptions: SequelizeOptions = { username: this.config.db.username, password: this.config.db.password, dialect: 'postgres', @@ -260,9 +269,18 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re port: this.config.db.port, logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false, timezone: 'America/New_York' + }; + this.instanceDB = new Sequelize({ + ...sharedDBOptions, + database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot' + }); + this.sharedDB = new Sequelize({ + ...sharedDBOptions, + database: 'bushbot-shared' }); - // global objects + // eslint-disable-next-line @typescript-eslint/no-unused-vars + /* global objects */ global.client = this; global.util = this.util; } @@ -321,7 +339,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re this.commandHandler.useTaskHandler(this.taskHandler); this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler); this.commandHandler.ignorePermissions = this.config.owners; - this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.global.superUsers])]; + this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.shared.superUsers])]; this.listenerHandler.setEmitters({ client: this, commandHandler: this.commandHandler, @@ -377,23 +395,36 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re /** * Connects to the database, initializes models, and creates tables if they do not exist. */ - private async dbPreInit() { + async dbPreInit() { + try { + await this.instanceDB.authenticate(); + GuildModel.initModel(this.instanceDB, this); + ModLog.initModel(this.instanceDB); + ActivePunishment.initModel(this.instanceDB); + Level.initModel(this.instanceDB); + StickyRole.initModel(this.instanceDB); + Reminder.initModel(this.instanceDB); + await this.instanceDB.sync({ alter: true }); // Sync all tables to fix everything if updated + await this.console.success('startup', `Successfully connected to <<instance database>>.`, false); + } catch (e) { + await this.console.error( + 'startup', + `Failed to connect to <<instance database>> with error:\n${util.inspect(e, { colors: true, depth: 1 })}`, + false + ); + process.exit(2); + } try { - await this.db.authenticate(); - Global.initModel(this.db); - GuildModel.initModel(this.db, this); - ModLog.initModel(this.db); - ActivePunishment.initModel(this.db); - Level.initModel(this.db); - StickyRole.initModel(this.db); - Stat.initModel(this.db); - Reminder.initModel(this.db); - await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated - await this.console.success('startup', `Successfully connected to <<database>>.`, false); + await this.sharedDB.authenticate(); + Stat.initModel(this.sharedDB); + Global.initModel(this.sharedDB); + Shared.initModel(this.sharedDB); + await this.sharedDB.sync({ alter: true }); // Sync all tables to fix everything if updated + await this.console.success('startup', `Successfully connected to <<shared database>>.`, false); } catch (e) { await this.console.error( 'startup', - `Failed to connect to <<database>> with error:\n${util.inspect(e, { colors: true, depth: 1 })}`, + `Failed to connect to <<shared database>> with error:\n${util.inspect(e, { colors: true, depth: 1 })}`, false ); process.exit(2); @@ -416,7 +447,6 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re }); try { - await this.dbPreInit(); await UpdateCacheTask.init(this); void this.console.success('startup', `Successfully created <<cache>>.`, false); this.stats.commandsUsed = await UpdateStatsTask.init(); @@ -443,7 +473,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re public override isSuperUser(user: BushUserResolvable): boolean { const userID = this.users.resolveId(user)!; - return !!client.cache?.global?.superUsers?.includes(userID) || this.config.owners.includes(userID); + return !!client.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID); } } diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 9a8e408..cdb883d 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -2,6 +2,8 @@ import { Arg, BushConstants, Global, + Shared, + SharedCache, type BushClient, type BushInspectOptions, type BushMessage, @@ -442,6 +444,12 @@ export class BushClientUtil extends ClientUtil { return key ? client.cache.global[key] : client.cache.global; } + public getShared(): SharedCache; + public getShared<K extends keyof SharedCache>(key: K): SharedCache[K]; + public getShared(key?: keyof SharedCache) { + return key ? client.cache.shared[key] : client.cache.shared; + } + /** * Add or remove an element from an array stored in the Globals database. * @param action Either `add` or `remove` an element. @@ -463,6 +471,25 @@ export class BushClientUtil extends ClientUtil { } /** + * Add or remove an element from an array stored in the Shared database. + * @param action Either `add` or `remove` an element. + * @param key The key of the element in the shared cache to update. + * @param value The value to add/remove from the array. + */ + public async insertOrRemoveFromShared<K extends keyof typeof client['cache']['shared']>( + action: 'add' | 'remove', + key: K, + value: typeof client['cache']['shared'][K][0] + ): Promise<Shared | void> { + const row = (await Shared.findByPk(0)) ?? (await Shared.create()); + const oldValue: any[] = row[key]; + const newValue = this.addOrRemoveFromArray(action, oldValue, value); + row[key] = newValue; + client.cache.shared[key] = newValue; + return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e)); + } + + /** * Updates an element in the Globals database. * @param key The key in the global cache to update. * @param value The value to set the key to. @@ -479,6 +506,21 @@ export class BushClientUtil extends ClientUtil { } /** + * Updates an element in the Shared database. + * @param key The key in the shared cache to update. + * @param value The value to set the key to. + */ + public async setShared<K extends keyof typeof client['cache']['shared']>( + key: K, + value: typeof client['cache']['shared'][K] + ): Promise<Shared | void> { + const row = (await Shared.findByPk(0)) ?? (await Shared.create()); + row[key] = value; + client.cache.shared[key] = value; + return await row.save().catch((e) => this.handleError('setShared', e)); + } + + /** * Add or remove an item from an array. All duplicates will be removed. * @param action Either `add` or `remove` an element. * @param array The array to add/remove an element from. diff --git a/src/lib/index.ts b/src/lib/index.ts index 8809e27..37bc443 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -73,6 +73,7 @@ export * from './models/Guild.js'; export * from './models/Level.js'; export * from './models/ModLog.js'; export * from './models/Reminder.js'; +export * from './models/Shared.js'; export * from './models/Stat.js'; export * from './models/StickyRole.js'; export * from './utils/AllowedMentions.js'; diff --git a/src/lib/models/Global.ts b/src/lib/models/Global.ts index 30a9d38..1deb090 100644 --- a/src/lib/models/Global.ts +++ b/src/lib/models/Global.ts @@ -2,12 +2,10 @@ import { type Snowflake } from 'discord.js'; import { type Sequelize } from 'sequelize'; import { BaseModel } from './BaseModel.js'; import { jsonArray } from './__helpers.js'; - const { DataTypes } = (await import('sequelize')).default; export interface GlobalModel { environment: 'production' | 'development' | 'beta'; - superUsers: Snowflake[]; disabledCommands: string[]; blacklistedUsers: Snowflake[]; blacklistedGuilds: Snowflake[]; @@ -16,7 +14,6 @@ export interface GlobalModel { export interface GlobalModelCreationAttributes { environment: 'production' | 'development' | 'beta'; - superUsers?: Snowflake[]; disabledCommands?: string[]; blacklistedUsers?: Snowflake[]; blacklistedGuilds?: Snowflake[]; @@ -30,11 +27,6 @@ export class Global extends BaseModel<GlobalModel, GlobalModelCreationAttributes public declare environment: 'production' | 'development' | 'beta'; /** - * Trusted users. - */ - public declare superUsers: Snowflake[]; - - /** * Globally disabled commands. */ public declare disabledCommands: string[]; @@ -62,7 +54,6 @@ export class Global extends BaseModel<GlobalModel, GlobalModelCreationAttributes Global.init( { environment: { type: DataTypes.STRING, primaryKey: true }, - superUsers: jsonArray('superUsers'), disabledCommands: jsonArray('disabledCommands'), blacklistedUsers: jsonArray('blacklistedUsers'), blacklistedGuilds: jsonArray('blacklistedGuilds'), diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index dfee37b..80bd119 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -5,7 +5,6 @@ import { type BadWords } from '../common/AutoMod.js'; import { type BushClient } from '../extensions/discord-akairo/BushClient.js'; import { BaseModel } from './BaseModel.js'; import { jsonArray, jsonObject } from './__helpers.js'; - const { DataTypes } = (await import('sequelize')).default; export interface GuildModel { diff --git a/src/lib/models/Level.ts b/src/lib/models/Level.ts index 2ed787d..e247779 100644 --- a/src/lib/models/Level.ts +++ b/src/lib/models/Level.ts @@ -1,7 +1,6 @@ import { type Snowflake } from 'discord.js'; import { type Sequelize } from 'sequelize'; import { BaseModel } from './BaseModel.js'; - const { DataTypes } = (await import('sequelize')).default; export interface LevelModel { diff --git a/src/lib/models/Shared.ts b/src/lib/models/Shared.ts new file mode 100644 index 0000000..dd7682b --- /dev/null +++ b/src/lib/models/Shared.ts @@ -0,0 +1,49 @@ +import { type Sequelize } from 'sequelize'; +import { BaseModel } from './BaseModel.js'; +import { jsonArray } from './__helpers.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface SharedModel { + primaryKey: 0; + superUsers: string[]; + badLinks: string[]; +} + +export interface SharedModelCreationAttributes { + primaryKey?: 0; + superUsers?: string[]; + badLinks?: string[]; +} + +export class Shared extends BaseModel<SharedModel, SharedModelCreationAttributes> implements SharedModel { + /** + * The primary key of the shared model. + */ + public declare primaryKey: 0; + + /** + * Trusted users. + */ + public declare superUsers: string[]; + + //todo + /** + * Bad links. + */ + public declare badLinks: string[]; + + /** + * Initializes the model. + * @param sequelize The sequelize instance. + */ + public static initModel(sequelize: Sequelize): void { + Shared.init( + { + primaryKey: { type: DataTypes.INTEGER, primaryKey: true, validate: { min: 0, max: 0 } }, + superUsers: jsonArray('superUsers'), + badLinks: jsonArray('badLinks') + }, + { sequelize, freezeTableName: true } + ); + } +} diff --git a/src/lib/models/__helpers.ts b/src/lib/models/__helpers.ts index 049dc00..bbfe328 100644 --- a/src/lib/models/__helpers.ts +++ b/src/lib/models/__helpers.ts @@ -1,5 +1,4 @@ import { type Model } from 'sequelize'; - const { DataTypes } = (await import('sequelize')).default; export function jsonParseGet(this: Model, key: string): any { diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts index cea0aea..5654495 100644 --- a/src/lib/utils/BushCache.ts +++ b/src/lib/utils/BushCache.ts @@ -3,15 +3,20 @@ import { Collection, type Snowflake } from 'discord.js'; export class BushCache { public global = new GlobalCache(); + public shared = new SharedCache(); public guilds = new GuildCache(); } export class GlobalCache { - public superUsers: Snowflake[] = []; public disabledCommands: string[] = []; public blacklistedChannels: Snowflake[] = []; public blacklistedGuilds: Snowflake[] = []; public blacklistedUsers: Snowflake[] = []; } +export class SharedCache { + public superUsers: Snowflake[] = []; + public badLinks: string[] = []; +} + export class GuildCache extends Collection<Snowflake, Guild> {} diff --git a/src/tasks/updateCache.ts b/src/tasks/updateCache.ts index 8f9cc5d..9084c1c 100644 --- a/src/tasks/updateCache.ts +++ b/src/tasks/updateCache.ts @@ -1,4 +1,4 @@ -import { Global, Guild, type BushClient } from '#lib'; +import { Global, Guild, Shared, type BushClient } from '#lib'; import { BushTask } from '../lib/extensions/discord-akairo/BushTask.js'; import config from './../config/options.js'; @@ -11,23 +11,39 @@ export default class UpdateCacheTask extends BushTask { } public override async exec() { - await UpdateCacheTask.updateGlobalCache(client); - await UpdateCacheTask.#updateGuildCache(client); + await Promise.all([ + UpdateCacheTask.#updateGlobalCache(client), + UpdateCacheTask.#updateSharedCache(client), + UpdateCacheTask.#updateGuildCache(client) + ]); void client.logger.verbose(`UpdateCache`, `Updated cache.`); } public static async init(client: BushClient) { - await UpdateCacheTask.updateGlobalCache(client); - await UpdateCacheTask.#updateGuildCache(client); + await Promise.all([ + UpdateCacheTask.#updateGlobalCache(client), + UpdateCacheTask.#updateSharedCache(client), + UpdateCacheTask.#updateGuildCache(client) + ]); } - private static async updateGlobalCache(client: BushClient) { + static async #updateGlobalCache(client: BushClient) { const environment = config.environment; const row: { [x: string]: any } = ((await Global.findByPk(environment)) ?? (await Global.create({ environment }))).toJSON(); for (const option in row) { if (Object.keys(client.cache.global).includes(option)) { client.cache.global[option as keyof typeof client.cache.global] = row[option]; + } + } + } + + static async #updateSharedCache(client: BushClient) { + const row: { [x: string]: any } = ((await Shared.findByPk(0)) ?? (await Shared.create())).toJSON(); + + for (const option in row) { + if (Object.keys(client.cache.shared).includes(option)) { + client.cache.shared[option as keyof typeof client.cache.shared] = row[option]; if (option === 'superUsers') client.superUserID = row[option]; } } diff --git a/src/tasks/updateSuperUsers.ts b/src/tasks/updateSuperUsers.ts deleted file mode 100644 index 8dcd00e..0000000 --- a/src/tasks/updateSuperUsers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BushTask, Global } from '#lib'; - -export default class UpdateSuperUsersTask extends BushTask { - public constructor() { - super('updateSuperUsers', { - delay: 10_000, // 10 seconds - runOnStart: true - }); - } - - public override async exec() { - const superUsers = client.guilds.cache - .get(client.config.supportGuild.id) - ?.members.cache.filter( - (member) => (member.roles.cache.has('865954009280938056') || member.permissions.has('ADMINISTRATOR')) && !member.user.bot - ) - .map((member) => member.id); - - const row = - (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment })); - - row.superUsers = superUsers ?? row.superUsers; - client.cache.global.superUsers = superUsers ?? row.superUsers; - await row.save(); - - void client.logger.verbose(`updateSuperUsers`, 'Updated superusers.'); - } -} |