From e6ac7e79b0ab18dcfa2e4e36bf42d21cf560887b Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 20:10:33 +0200 Subject: [PATCH 1/6] refactor: handle checks in Command structure with bitflags --- src/commands/admin/CrashCommand.ts | 18 +++++----- src/commands/admin/DevResetCommand.ts | 14 ++++---- src/commands/admin/SyncCommand.ts | 20 ++++++----- src/commands/admin/UpdateGeminiKeyCommand.ts | 14 ++++---- src/commands/moderation/BanCommand.ts | 13 +++---- src/commands/moderation/ClearCommand.ts | 11 ++---- src/commands/moderation/KickCommand.ts | 13 +++---- src/commands/moderation/LockCommand.ts | 13 +++---- src/commands/moderation/MuteCommand.ts | 13 +++---- src/commands/moderation/PurgeCommand.ts | 19 ++++------- src/commands/moderation/RoleCommand.ts | 13 +++---- src/commands/moderation/UnmuteCommand.ts | 11 ++---- src/commands/utils/PingCommand.ts | 3 +- src/structures/Command.ts | 36 ++++++++++++++++++++ 14 files changed, 106 insertions(+), 105 deletions(-) diff --git a/src/commands/admin/CrashCommand.ts b/src/commands/admin/CrashCommand.ts index d28ef8f..273ced7 100644 --- a/src/commands/admin/CrashCommand.ts +++ b/src/commands/admin/CrashCommand.ts @@ -1,8 +1,8 @@ import { PermissionsBitField } from 'discord.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { ExtendedClient } from '@structures/Client.js'; import { Embeds } from '@utils/Embeds.js'; -import { Constants } from '@utils/Constants.js'; +import * as ds from '@data/DataStore.js'; export default class CrashCommand extends Command { constructor() { @@ -10,21 +10,21 @@ export default class CrashCommand extends Command { name: 'crash', description: 'Crash the bot', requiredPermissions: [PermissionsBitField.Flags.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; + const author = context.author!; + const data = ds.getDataForId(context.guild); - if (!author) { - await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], + if (!data) { + return await context.reply({ + embeds: [Embeds.error('Something went wrong, please try again later.')], }); - - return; } - if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { + if (!data.trusted.includes(author.id)) { await context.reply({ embeds: [Embeds.error('You are not authorized to use this command.')], }); diff --git a/src/commands/admin/DevResetCommand.ts b/src/commands/admin/DevResetCommand.ts index 16300d7..e07eb7e 100644 --- a/src/commands/admin/DevResetCommand.ts +++ b/src/commands/admin/DevResetCommand.ts @@ -1,8 +1,8 @@ import { PermissionsBitField } from 'discord.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { ExtendedClient } from '@structures/Client.js'; import { Embeds } from '@utils/Embeds.js'; -import { Constants } from '@utils/Constants.js'; +import * as ds from '@data/DataStore.js'; export default class DevResetCommand extends Command { constructor() { @@ -10,21 +10,23 @@ export default class DevResetCommand extends Command { name: 'devreset', description: 'Reset the chat bot', requiredPermissions: [PermissionsBitField.Flags.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; + const author = context.author!; + const data = ds.getDataForId(context.guild); - if (!author) { + if (!data) { await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], + embeds: [Embeds.error('Something went wrong, please try again later.')], }); return; } - if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { + if (!data.trusted.includes(author.id)) { await context.reply({ embeds: [Embeds.error('You are not authorized to use this command.')], }); diff --git a/src/commands/admin/SyncCommand.ts b/src/commands/admin/SyncCommand.ts index 6bfc390..79de460 100644 --- a/src/commands/admin/SyncCommand.ts +++ b/src/commands/admin/SyncCommand.ts @@ -1,8 +1,8 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; -import { Constants } from '@utils/Constants.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; +import * as ds from '@data/DataStore.js'; export default class SyncCommand extends Command { constructor() { @@ -10,27 +10,29 @@ export default class SyncCommand extends Command { name: 'sync', description: "Cleanup each member's roles", requiredPermissions: [PermissionFlagsBits.Administrator], + checkFlags: CommandCheckFlags.Guild, }); } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; + const guild = context.guild!; + const data = ds.getDataForId(guild); - if (!guild) { + if (!data) { return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], + embeds: [Embeds.error('Something went wrong, please try again later.')], }); } const members = await guild.members.fetch(); - const communityRole = guild.roles.cache.get(Constants.ROLES.COMMUNITY); + const communityRole = guild.roles.cache.get(data.roles.community); const ignoredRoles = new Set( [ guild.id, // @everyone role guild.roles.premiumSubscriberRole?.id, - guild.roles.cache.get(Constants.ROLES.UPDATES)?.id, - guild.roles.cache.get(Constants.ROLES.QOTD_PING)?.id, - guild.roles.cache.get(Constants.ROLES.SUPPORT)?.id, + guild.roles.cache.get(data.roles.updates)?.id, + guild.roles.cache.get(data.roles.qotd)?.id, + guild.roles.cache.get(data.roles.support)?.id, ].filter((id) => id != null) ); diff --git a/src/commands/admin/UpdateGeminiKeyCommand.ts b/src/commands/admin/UpdateGeminiKeyCommand.ts index a340b1a..45a55b7 100644 --- a/src/commands/admin/UpdateGeminiKeyCommand.ts +++ b/src/commands/admin/UpdateGeminiKeyCommand.ts @@ -1,9 +1,9 @@ import { MessageFlags, PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Argument } from '@structures/Argument.js'; import { Embeds } from '@utils/Embeds.js'; -import { Constants } from '@utils/Constants.js'; +import * as ds from '@data/DataStore.js'; export default class UpdateGeminiKeyCommand extends Command { constructor() { @@ -11,6 +11,7 @@ export default class UpdateGeminiKeyCommand extends Command { name: 'updategeminikey', description: 'Update the Gemini API key', requiredPermissions: [PermissionFlagsBits.Administrator], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'key', @@ -23,17 +24,18 @@ export default class UpdateGeminiKeyCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const author = context.interaction?.user || context.message?.author; + const author = context.author!; + const data = ds.getDataForId(context.guild); - if (!author) { + if (!data) { await context.reply({ - embeds: [Embeds.error('Unable to identify the command author.')], + embeds: [Embeds.error('Something went wrong, please try again later.')], }); return; } - if (!Constants.TRUSTED_USER_IDS.includes(author.id)) { + if (!data.trusted.includes(author.id)) { await context.reply({ embeds: [Embeds.error('You are not authorized to use this command.')], }); diff --git a/src/commands/moderation/BanCommand.ts b/src/commands/moderation/BanCommand.ts index c6d6fe6..1966e42 100644 --- a/src/commands/moderation/BanCommand.ts +++ b/src/commands/moderation/BanCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class BanCommand extends Command { name: 'ban', description: 'Ban a user from the server', requiredPermissions: [PermissionFlagsBits.BanMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class BanCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/ClearCommand.ts b/src/commands/moderation/ClearCommand.ts index 8eea35e..f5056c0 100644 --- a/src/commands/moderation/ClearCommand.ts +++ b/src/commands/moderation/ClearCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class ClearCommand extends Command { name: 'clear', description: 'Clear messages from a channel', requiredPermissions: [PermissionFlagsBits.ManageMessages], + checkFlags: CommandCheckFlags.None, args: [ new Argument({ name: 'amount', @@ -22,14 +23,6 @@ export default class ClearCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } - const amount = context.args.amount as number; if (!amount || isNaN(amount) || amount < 1 || amount > 100) { diff --git a/src/commands/moderation/KickCommand.ts b/src/commands/moderation/KickCommand.ts index 942bbce..6b599b0 100644 --- a/src/commands/moderation/KickCommand.ts +++ b/src/commands/moderation/KickCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class KickCommand extends Command { name: 'kick', description: 'Kick a user from the server', requiredPermissions: [PermissionFlagsBits.KickMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class KickCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/LockCommand.ts b/src/commands/moderation/LockCommand.ts index 2762b46..94d1620 100644 --- a/src/commands/moderation/LockCommand.ts +++ b/src/commands/moderation/LockCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class LockCommand extends Command { name: 'lock', description: 'Lock a channel', requiredPermissions: [PermissionFlagsBits.ManageChannels], + checkFlags: CommandCheckFlags.Guild, args: [ new Argument({ name: 'channel', @@ -22,16 +23,10 @@ export default class LockCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; const channel = context.args.channel - ? guild?.channels.cache.get(context.args.channel as string) + ? guild.channels.cache.get(context.args.channel as string) : (context.interaction?.channel ?? context.message?.channel); if (!channel || !channel.isTextBased()) { diff --git a/src/commands/moderation/MuteCommand.ts b/src/commands/moderation/MuteCommand.ts index 6557129..c63023c 100644 --- a/src/commands/moderation/MuteCommand.ts +++ b/src/commands/moderation/MuteCommand.ts @@ -1,6 +1,6 @@ import { Argument } from '@structures/Argument.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { PermissionsBitField } from 'discord.js'; import { Embeds } from '@utils/Embeds.js'; @@ -10,6 +10,7 @@ export default class MuteCommand extends Command { name: 'mute', description: 'Mute a user', requiredPermissions: [PermissionsBitField.Flags.ModerateMembers], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -34,14 +35,8 @@ export default class MuteCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/PurgeCommand.ts b/src/commands/moderation/PurgeCommand.ts index 7f99dc7..88019e6 100644 --- a/src/commands/moderation/PurgeCommand.ts +++ b/src/commands/moderation/PurgeCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits, TextChannel } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class PurgeCommand extends Command { name: 'purge', description: 'Purge and recreate a channel', requiredPermissions: [PermissionFlagsBits.ManageChannels], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'channel', @@ -22,17 +23,11 @@ export default class PurgeCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const originalChannel = context.args.channel - ? guild?.channels.cache.get(context.args.channel as string) + ? guild.channels.cache.get(context.args.channel as string) : (context.interaction?.channel ?? context.message?.channel); if (!originalChannel || !originalChannel.isTextBased()) { @@ -56,10 +51,10 @@ export default class PurgeCommand extends Command { } await newChannel.setPosition(textChannel.position); - await textChannel.delete(`Purged by ${author?.tag}`); + await textChannel.delete(`Purged by ${author.tag}`); await newChannel.send({ - embeds: [Embeds.success(`Channel purged by ${author?.tag}`)], + embeds: [Embeds.success(`Channel purged by ${author.tag}`)], }); } } diff --git a/src/commands/moderation/RoleCommand.ts b/src/commands/moderation/RoleCommand.ts index ff64d7e..0ad988f 100644 --- a/src/commands/moderation/RoleCommand.ts +++ b/src/commands/moderation/RoleCommand.ts @@ -1,6 +1,6 @@ import { PermissionFlagsBits } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; import { Argument } from '@structures/Argument.js'; @@ -10,6 +10,7 @@ export default class RoleCommand extends Command { name: 'role', description: 'Give or remove a role from a user', requiredPermissions: [PermissionFlagsBits.ManageRoles], + checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,14 +29,8 @@ export default class RoleCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - const author = context.interaction?.user ?? context.message?.author; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; + const author = context.author!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/moderation/UnmuteCommand.ts b/src/commands/moderation/UnmuteCommand.ts index 3be5b1d..fc361fc 100644 --- a/src/commands/moderation/UnmuteCommand.ts +++ b/src/commands/moderation/UnmuteCommand.ts @@ -1,6 +1,6 @@ import { Argument } from '@structures/Argument.js'; import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { PermissionsBitField } from 'discord.js'; import { Embeds } from '@utils/Embeds.js'; @@ -10,6 +10,7 @@ export default class UnmuteCommand extends Command { name: 'unmute', description: 'Unmute a user', requiredPermissions: [PermissionsBitField.Flags.ModerateMembers], + checkFlags: CommandCheckFlags.Guild, args: [ new Argument({ name: 'user', @@ -28,13 +29,7 @@ export default class UnmuteCommand extends Command { } public async execute(client: ExtendedClient, context: CommandContext): Promise { - const guild = context.interaction?.guild ?? context.message?.guild; - - if (!guild) { - return await context.reply({ - embeds: [Embeds.error('This command can only be used in a server.')], - }); - } + const guild = context.guild!; const user = guild.members.cache.get(context.args.user as string); diff --git a/src/commands/utils/PingCommand.ts b/src/commands/utils/PingCommand.ts index 1365ad3..2478329 100644 --- a/src/commands/utils/PingCommand.ts +++ b/src/commands/utils/PingCommand.ts @@ -1,5 +1,5 @@ import { ExtendedClient } from '@structures/Client.js'; -import { Command, CommandContext } from '@structures/Command.js'; +import { Command, CommandContext, CommandCheckFlags } from '@structures/Command.js'; import { Embeds } from '@utils/Embeds.js'; export default class PingCommand extends Command { @@ -7,6 +7,7 @@ export default class PingCommand extends Command { super({ name: 'ping', description: 'Check the bot latency', + checkFlags: CommandCheckFlags.None, }); } diff --git a/src/structures/Command.ts b/src/structures/Command.ts index 842b675..940456e 100644 --- a/src/structures/Command.ts +++ b/src/structures/Command.ts @@ -2,6 +2,7 @@ import { ChatInputCommandInteraction, Guild, Message, + User, SlashCommandBuilder, MessageCreateOptions, InteractionReplyOptions, @@ -12,6 +13,7 @@ import { } from 'discord.js'; import { ExtendedClient } from '@structures/Client.js'; import { Argument, ArgumentType } from '@structures/Argument.js'; +import { Embeds } from '@utils/Embeds.js'; import { Utils } from '@utils/Utils.js'; export interface CommandOptions { @@ -19,12 +21,22 @@ export interface CommandOptions { description: string; args?: Argument[]; requiredPermissions?: PermissionResolvable[]; + checkFlags?: CommandCheckFlags; +} + +export enum CommandCheckFlags { + None = 0, + Author = 1 << 0, + Guild = 1 << 1, + Default = Author | Guild, } export interface CommandContext { client: ExtendedClient; interaction?: ChatInputCommandInteraction; message?: Message; + author?: User; + guild?: Guild; args: Record; reply(content: string | MessageCreateOptions | InteractionReplyOptions): Promise; deferReply(ephemeral?: boolean): Promise; @@ -37,12 +49,14 @@ export abstract class Command { public readonly description: string; public readonly args: Argument[]; public readonly requiredPermissions: PermissionResolvable[]; + public readonly checkFlags: CommandCheckFlags; constructor(options: CommandOptions) { this.name = options.name; this.description = options.description; this.args = options.args ?? []; this.requiredPermissions = options.requiredPermissions ?? []; + this.checkFlags = options.checkFlags ?? CommandCheckFlags.Default; } public buildSlashCommand(): SlashCommandBuilder { @@ -61,6 +75,26 @@ export abstract class Command { return builder; } + public async run(client: ExtendedClient, context: CommandContext): Promise { + if (this.checkFlags & CommandCheckFlags.Author && !context.author) { + await context.reply({ + embeds: [Embeds.error('Unable to identify the command author.')], + }); + + return; + } + + if (this.checkFlags & CommandCheckFlags.Guild && !context.guild) { + await context.reply({ + embeds: [Embeds.error('This command can only be used in a server.')], + }); + + return; + } + + await this.execute(client, context); + } + private addArgumentToBuilder(builder: SlashCommandBuilder, arg: Argument): void { const methodName = this.getBuilderMethodName(arg.type); const methodKey = `add${methodName}Option` as const; @@ -164,6 +198,8 @@ export abstract class Command { client, interaction, message, + author: interaction?.user ?? message?.author, + guild: interaction?.guild ?? message?.guild ?? undefined, args, reply: async (content: string | MessageCreateOptions | InteractionReplyOptions) => { if (interaction) { From 31b6c9d1454c0196bba0d3ee97c4527fcc03f384 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 21:35:28 +0200 Subject: [PATCH 2/6] feature: multi guild support --- package-lock.json | 6 ++ {data => resources}/context.txt | 0 resources/guildSettings.json | 40 +++++++++++ src/commands/admin/CrashCommand.ts | 2 +- src/commands/admin/DevResetCommand.ts | 2 +- src/commands/admin/SyncCommand.ts | 2 +- src/commands/admin/UpdateGeminiKeyCommand.ts | 2 +- src/data/Config.ts | 2 + src/data/DataStore.ts | 71 ++++++++++++++++++++ src/events/ClientReadyEvent.ts | 3 +- src/events/InteractionCreateEvent.ts | 2 +- src/events/MessageCreateEvent.ts | 2 +- src/events/logging/GuildMemberAddEvent.ts | 1 - src/events/logging/GuildMemberRemoveEvent.ts | 1 - src/structures/Client.ts | 16 ++--- src/structures/CommandManager.ts | 20 ++---- src/utils/ChatBot.ts | 5 +- src/utils/Constants.ts | 23 ------- src/utils/SmeeClient.ts | 18 ++--- tsconfig.json | 3 +- 20 files changed, 154 insertions(+), 67 deletions(-) rename {data => resources}/context.txt (100%) create mode 100644 resources/guildSettings.json create mode 100644 src/data/Config.ts create mode 100644 src/data/DataStore.ts delete mode 100644 src/utils/Constants.ts diff --git a/package-lock.json b/package-lock.json index add980d..28a8664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1002,6 +1002,7 @@ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", @@ -1229,6 +1230,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1746,6 +1748,7 @@ "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -2869,6 +2872,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3468,6 +3472,7 @@ "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3590,6 +3595,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/data/context.txt b/resources/context.txt similarity index 100% rename from data/context.txt rename to resources/context.txt diff --git a/resources/guildSettings.json b/resources/guildSettings.json new file mode 100644 index 0000000..e6b706e --- /dev/null +++ b/resources/guildSettings.json @@ -0,0 +1,40 @@ +{ + "1325571365079879774": { + "channels": { + "logging": "1442896999237030031", + "commits": "1489996651551522816", + "errors": "1491479994994659428" + }, + "roles": { + "updates": "1445497401614925914", + "qotd": "1445497592573067546", + "support": "1445887485853962300", + "community": "1416251642185121823" + }, + "trusted": [ + "1441859003708866601", + "768481984242253904", + "855798460593733652", + "1382022366040686763" + ] + }, + "1310540137096282122": { + "channels": { + "logging": "1492607126638563459", + "commits": "1492607196301758525", + "errors": "1492607206346981557" + }, + "roles": { + "updates": "1492607385112674354", + "qotd": "1492607410483761232", + "support": "1492607474996346952", + "community": "1492607539919978646" + }, + "trusted": [ + "1441859003708866601", + "768481984242253904", + "855798460593733652", + "1382022366040686763" + ] + } +} diff --git a/src/commands/admin/CrashCommand.ts b/src/commands/admin/CrashCommand.ts index 273ced7..7a00a9a 100644 --- a/src/commands/admin/CrashCommand.ts +++ b/src/commands/admin/CrashCommand.ts @@ -16,7 +16,7 @@ export default class CrashCommand extends Command { public async execute(client: ExtendedClient, context: CommandContext): Promise { const author = context.author!; - const data = ds.getDataForId(context.guild); + const data = ds.getDataFromContext(context); if (!data) { return await context.reply({ diff --git a/src/commands/admin/DevResetCommand.ts b/src/commands/admin/DevResetCommand.ts index e07eb7e..38eaf27 100644 --- a/src/commands/admin/DevResetCommand.ts +++ b/src/commands/admin/DevResetCommand.ts @@ -16,7 +16,7 @@ export default class DevResetCommand extends Command { public async execute(client: ExtendedClient, context: CommandContext): Promise { const author = context.author!; - const data = ds.getDataForId(context.guild); + const data = ds.getDataFromContext(context); if (!data) { await context.reply({ diff --git a/src/commands/admin/SyncCommand.ts b/src/commands/admin/SyncCommand.ts index 79de460..a1f7f4e 100644 --- a/src/commands/admin/SyncCommand.ts +++ b/src/commands/admin/SyncCommand.ts @@ -16,7 +16,7 @@ export default class SyncCommand extends Command { public async execute(client: ExtendedClient, context: CommandContext): Promise { const guild = context.guild!; - const data = ds.getDataForId(guild); + const data = ds.getDataFromContext(context); if (!data) { return await context.reply({ diff --git a/src/commands/admin/UpdateGeminiKeyCommand.ts b/src/commands/admin/UpdateGeminiKeyCommand.ts index 45a55b7..35218a2 100644 --- a/src/commands/admin/UpdateGeminiKeyCommand.ts +++ b/src/commands/admin/UpdateGeminiKeyCommand.ts @@ -25,7 +25,7 @@ export default class UpdateGeminiKeyCommand extends Command { public async execute(client: ExtendedClient, context: CommandContext): Promise { const author = context.author!; - const data = ds.getDataForId(context.guild); + const data = ds.getDataFromContext(context); if (!data) { await context.reply({ diff --git a/src/data/Config.ts b/src/data/Config.ts new file mode 100644 index 0000000..27869b0 --- /dev/null +++ b/src/data/Config.ts @@ -0,0 +1,2 @@ +export const GUILD_SETTINGS_FILE = 'resources/guildSettings.json'; +export const COBALT_GUILD_ID = '1325571365079879774'; diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts new file mode 100644 index 0000000..68de838 --- /dev/null +++ b/src/data/DataStore.ts @@ -0,0 +1,71 @@ +type ChannelTypes = 'logging' | 'commits' | 'errors'; +type RoleTypes = 'updates' | 'qotd' | 'support' | 'community'; +import * as fs from 'fs'; +import { GUILD_SETTINGS_FILE } from '@data/Config.js'; +import { CommandContext } from '@structures/Command.js'; +import { Guild } from 'discord.js'; + +export class GuildData { + readonly guildId: string; + channels: Record; + roles: Record; + trusted: string[]; + + constructor(guildId: string) { + this.guildId = guildId; + this.channels = { + logging: '', + commits: '', + errors: '', + }; + this.roles = { + updates: '', + qotd: '', + support: '', + community: '', + }; + this.trusted = []; + } + + toJson(): string { + return JSON.stringify( + { + [this.guildId]: { + channels: this.channels, + roles: this.roles, + trusted: this.trusted, + }, + }, + null, + 2 + ); + } +} + +export let dataStore: GuildData[] = loadFromJson(GUILD_SETTINGS_FILE); + +function loadFromJson(jsonFilePath: string) { + const fileContent = fs.readFileSync(jsonFilePath, 'utf-8'); + const data = JSON.parse(fileContent); + const values = []; + for (const guildId of Object.keys(data)) { + const guildObj = data[guildId]; + const guildData = new GuildData(guildId); + guildData.channels = guildObj.channels; + guildData.roles = guildObj.roles; + guildData.trusted = guildObj.trusted; + values.push(guildData); + } + return values; +} + +export function getDataFromContext(context: CommandContext): GuildData | undefined { + if (!context.guild) { + return undefined; + } + return getDataForGuild(context.guild.id); +} + +export function getDataForGuild(guildId: string): GuildData | undefined { + return dataStore.find((guild) => guild.guildId === guildId); +} diff --git a/src/events/ClientReadyEvent.ts b/src/events/ClientReadyEvent.ts index 1cefe70..78e76fb 100644 --- a/src/events/ClientReadyEvent.ts +++ b/src/events/ClientReadyEvent.ts @@ -4,7 +4,6 @@ import { Logger } from '@utils/Logger.js'; import { CommandManager } from '@structures/CommandManager.js'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { Constants } from '@utils/Constants.js'; export default class ClientReadyEvent extends Event<'clientReady'> { constructor() { @@ -18,7 +17,7 @@ export default class ClientReadyEvent extends Event<'clientReady'> { Logger.success(`Logged in as ${client.user?.tag}`); const commandsDirectory = join(dirname(fileURLToPath(import.meta.url)), '..', 'commands'); - const commandManager = new CommandManager(client, Constants.GUILD_ID); + const commandManager = new CommandManager(client); await commandManager.loadCommands(commandsDirectory); diff --git a/src/events/InteractionCreateEvent.ts b/src/events/InteractionCreateEvent.ts index ddb2ea5..5edefbb 100644 --- a/src/events/InteractionCreateEvent.ts +++ b/src/events/InteractionCreateEvent.ts @@ -48,7 +48,7 @@ export default class InteractionCreateEvent extends Event { } const context = command.createContext(client, args, interaction); - await command.execute(client, context); + await command.run(client, context); } catch (error) { const errorMessage = { embeds: [Embeds.error(`${error instanceof Error ? error.message : 'Unknown error'}`)], diff --git a/src/events/MessageCreateEvent.ts b/src/events/MessageCreateEvent.ts index 63d3951..ca74e66 100644 --- a/src/events/MessageCreateEvent.ts +++ b/src/events/MessageCreateEvent.ts @@ -33,7 +33,7 @@ export default class MessageCreateEvent extends Event<'messageCreate'> { const parsedArgs = await command.parseChatArgs(args, message.guild ?? undefined); const context = command.createContext(client, parsedArgs, undefined, message); - await command.execute(client, context); + await command.run(client, context); } catch (error) { await message.reply({ embeds: [Embeds.error(`${error instanceof Error ? error.message : 'Unknown error'}`)], diff --git a/src/events/logging/GuildMemberAddEvent.ts b/src/events/logging/GuildMemberAddEvent.ts index 6f49604..fe28afb 100644 --- a/src/events/logging/GuildMemberAddEvent.ts +++ b/src/events/logging/GuildMemberAddEvent.ts @@ -1,7 +1,6 @@ import { Event } from '@structures/Event.js'; import { ExtendedClient } from '@structures/Client.js'; import { EmbedBuilder, GuildMember } from 'discord.js'; -import { Constants } from '@utils/Constants.js'; export default class GuildMemberAddEvent extends Event<'guildMemberAdd'> { constructor() { diff --git a/src/events/logging/GuildMemberRemoveEvent.ts b/src/events/logging/GuildMemberRemoveEvent.ts index d0a2840..873f4f9 100644 --- a/src/events/logging/GuildMemberRemoveEvent.ts +++ b/src/events/logging/GuildMemberRemoveEvent.ts @@ -1,7 +1,6 @@ import { Event } from '@structures/Event.js'; import { ExtendedClient } from '@structures/Client.js'; import { EmbedBuilder, GuildMember } from 'discord.js'; -import { Constants } from '@utils/Constants.js'; export default class GuildMemberRemoveEvent extends Event<'guildMemberRemove'> { constructor() { diff --git a/src/structures/Client.ts b/src/structures/Client.ts index b4bda24..0605b1b 100644 --- a/src/structures/Client.ts +++ b/src/structures/Client.ts @@ -4,11 +4,12 @@ import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { Event } from '@structures/Event.js'; import { CommandManager } from '@structures/CommandManager.js'; -import { Constants } from '@utils/Constants.js'; import { SmeeClient } from '@utils/SmeeClient.js'; import { ChatBot } from '@utils/ChatBot.js'; import { Embeds } from '@utils/Embeds.js'; import { Logger } from '@utils/Logger.js'; +import { COBALT_GUILD_ID } from '@data/Config.js'; +import { dataStore } from '@data/DataStore.js'; export interface ExtendedClientOptions { token: string; @@ -45,7 +46,9 @@ export class ExtendedClient extends Client { this.chatBot = new ChatBot(this, extendedClientOptions.mistralApiKey); this.smeeClient = new SmeeClient({ source: extendedClientOptions.smeeUrl, - channelId: Constants.CHANNELS.COMMITS_CHANNEL, + channelIds: dataStore + .map((guildStore) => guildStore.channels.commits) + .filter((channelId) => channelId.length > 0), target: 'http://localhost:6242/webhook', port: 6242, }); @@ -55,7 +58,7 @@ export class ExtendedClient extends Client { public updatePresence(): void { const cobaltGuild = this.guilds.cache.find((guild) => { - return guild.id == Constants.GUILD_ID; + return guild.id == COBALT_GUILD_ID; }); this.user?.setPresence({ @@ -69,16 +72,13 @@ export class ExtendedClient extends Client { }); } - public async logError(message: string): Promise { + public static async logError(message: string, channel: TextChannel): Promise { try { - const guild = this.guilds.cache.get(Constants.GUILD_ID); - const channel = guild?.channels.cache.get(Constants.CHANNELS.BOT_ERRORS); - if (!channel || !channel.isTextBased()) { return; } - await (channel as TextChannel).send({ + await channel.send({ embeds: [Embeds.error(message)], }); } catch (error) { diff --git a/src/structures/CommandManager.ts b/src/structures/CommandManager.ts index e1b10f5..a120efe 100644 --- a/src/structures/CommandManager.ts +++ b/src/structures/CommandManager.ts @@ -9,11 +9,9 @@ import { Logger } from '@utils/Logger.js'; export class CommandManager { private commands: Map = new Map(); private readonly client: ExtendedClient; - private readonly guildId?: string; - constructor(client: ExtendedClient, guildId?: string) { + constructor(client: ExtendedClient) { this.client = client; - this.guildId = guildId; } public async loadCommands(commandsDirectory: string): Promise { @@ -67,19 +65,11 @@ export class CommandManager { try { Logger.info(`Registering ${slashCommands.length} slash commands...`); - if (this.guildId) { - await rest.put(Routes.applicationGuildCommands(this.client.user!.id, this.guildId), { - body: slashCommands, - }); + await rest.put(Routes.applicationCommands(this.client.user!.id), { + body: slashCommands, + }); - Logger.success(`Slash commands registered to guild: ${this.guildId}`); - } else { - await rest.put(Routes.applicationCommands(this.client.user!.id), { - body: slashCommands, - }); - - Logger.success('Slash commands registered globally'); - } + Logger.success('Slash commands registered globally'); } catch (error) { Logger.error( `Failed to register slash commands: ${error instanceof Error ? error.message : String(error)}` diff --git a/src/utils/ChatBot.ts b/src/utils/ChatBot.ts index edbf875..8a2fd82 100644 --- a/src/utils/ChatBot.ts +++ b/src/utils/ChatBot.ts @@ -1,7 +1,7 @@ import { Mistral } from '@mistralai/mistralai'; import { readFileSync } from 'node:fs'; import { ExtendedClient } from '@structures/Client.js'; -import { Constants } from '@utils/Constants.js'; +import { COBALT_GUILD_ID } from '@data/Config.js'; type ChatAuthor = { id: string; @@ -37,7 +37,8 @@ export class ChatBot { public reset(): void { this.context = - readFileSync(new URL('../../data/context.txt', import.meta.url), 'utf8').toString() || ''; + readFileSync(new URL('../../resources/context.txt', import.meta.url), 'utf8').toString() || + ''; this.messages = [ { diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts deleted file mode 100644 index 7620b97..0000000 --- a/src/utils/Constants.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class Constants { - public static readonly GUILD_ID = '1325571365079879774'; // Cobalt Guild ID - - public static readonly ROLES = { - UPDATES: '1445497401614925914', - QOTD_PING: '1445497592573067546', - SUPPORT: '1445887485853962300', - COMMUNITY: '1416251642185121823', - }; - - public static readonly CHANNELS = { - LOGGING_CHANNEL: '1442896999237030031', - COMMITS_CHANNEL: '1489996651551522816', - BOT_ERRORS: '1491479994994659428', - }; - - public static readonly TRUSTED_USER_IDS = [ - '1441859003708866601', - '768481984242253904', - '855798460593733652', - '1382022366040686763', - ]; -} diff --git a/src/utils/SmeeClient.ts b/src/utils/SmeeClient.ts index 2019cc7..4e8e806 100644 --- a/src/utils/SmeeClient.ts +++ b/src/utils/SmeeClient.ts @@ -30,20 +30,20 @@ interface GitHubPushPayload { export interface SmeeClientOptions { source: string; target: string; - channelId: string; + channelIds: string[]; port: number; } export class SmeeClient { private readonly source?: string; private readonly target: string; - private readonly channelId: string; + private readonly channelIds: string[]; private readonly port: number; constructor(options: SmeeClientOptions) { this.source = options.source; this.target = options.target; - this.channelId = options.channelId; + this.channelIds = options.channelIds; this.port = options.port; } @@ -166,12 +166,14 @@ export class SmeeClient { ]) .setColor(0x4682b4); - const channel = await client.channels.fetch(this.channelId); + for (const channelId of this.channelIds) { + const channel = await client.channels.fetch(channelId); - if (channel?.isTextBased() && 'send' in channel) { - await channel.send({ - embeds: [embed], - }); + if (channel?.isTextBased() && 'send' in channel) { + await channel.send({ + embeds: [embed], + }); + } } res.status(200).send('Webhook received and processed'); diff --git a/tsconfig.json b/tsconfig.json index 0e38efc..32b305d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "@commands/*": ["./src/commands/*"], "@events/*": ["./src/events/*"], "@structures/*": ["./src/structures/*"], - "@utils/*": ["./src/utils/*"] + "@utils/*": ["./src/utils/*"], + "@data/*": ["./src/data/*"] } }, "include": ["src"], From 22fcbbf3a03ffce959d257f0d8b45836c30d54f1 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 21:46:24 +0200 Subject: [PATCH 3/6] feature: json validation --- src/data/DataStore.ts | 10 ++-- src/errors/RuntimeException.ts | 6 ++ src/structures/CommandManager.ts | 5 +- src/types/JsonValidationTypes.ts | 12 ++++ src/utils/JsonValidation.ts | 98 ++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 src/errors/RuntimeException.ts create mode 100644 src/types/JsonValidationTypes.ts create mode 100644 src/utils/JsonValidation.ts diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts index 68de838..c3ea1eb 100644 --- a/src/data/DataStore.ts +++ b/src/data/DataStore.ts @@ -3,7 +3,7 @@ type RoleTypes = 'updates' | 'qotd' | 'support' | 'community'; import * as fs from 'fs'; import { GUILD_SETTINGS_FILE } from '@data/Config.js'; import { CommandContext } from '@structures/Command.js'; -import { Guild } from 'discord.js'; +import { validateGuildSettingsFile } from '../utils/JsonValidation.js'; export class GuildData { readonly guildId: string; @@ -44,10 +44,12 @@ export class GuildData { export let dataStore: GuildData[] = loadFromJson(GUILD_SETTINGS_FILE); -function loadFromJson(jsonFilePath: string) { +function loadFromJson(jsonFilePath: string): GuildData[] { const fileContent = fs.readFileSync(jsonFilePath, 'utf-8'); - const data = JSON.parse(fileContent); - const values = []; + const data = JSON.parse(fileContent) as unknown; + validateGuildSettingsFile(data, jsonFilePath); + + const values: GuildData[] = []; for (const guildId of Object.keys(data)) { const guildObj = data[guildId]; const guildData = new GuildData(guildId); diff --git a/src/errors/RuntimeException.ts b/src/errors/RuntimeException.ts new file mode 100644 index 0000000..1df8da0 --- /dev/null +++ b/src/errors/RuntimeException.ts @@ -0,0 +1,6 @@ +export class RuntimeException extends Error { + constructor(message: string) { + super(message); + this.name = 'RuntimeException'; + } +} diff --git a/src/structures/CommandManager.ts b/src/structures/CommandManager.ts index a120efe..0718fac 100644 --- a/src/structures/CommandManager.ts +++ b/src/structures/CommandManager.ts @@ -16,6 +16,8 @@ export class CommandManager { public async loadCommands(commandsDirectory: string): Promise { const commandFiles = await this.getCommandFiles(commandsDirectory); + const separator: string = ', '; + let loadedCommands: string = ''; for (const commandPath of commandFiles) { const commandModule = await import(pathToFileURL(commandPath).href); @@ -31,8 +33,9 @@ export class CommandManager { } this.commands.set(command.name, command); - Logger.info(`Loaded command: ${command.name}`); + loadedCommands += command.name + separator; } + Logger.info(`Loaded commands: ${loadedCommands.slice(0, 0 - separator.length)}`); } private async getCommandFiles(directory: string): Promise { diff --git a/src/types/JsonValidationTypes.ts b/src/types/JsonValidationTypes.ts new file mode 100644 index 0000000..fd3f26c --- /dev/null +++ b/src/types/JsonValidationTypes.ts @@ -0,0 +1,12 @@ +export type ChannelTypes = 'logging' | 'commits' | 'errors'; + +export type RoleTypes = 'updates' | 'qotd' | 'support' | 'community'; + +export type GuildSettingsFile = Record< + string, + { + channels: Record; + roles: Record; + trusted: string[]; + } +>; diff --git a/src/utils/JsonValidation.ts b/src/utils/JsonValidation.ts new file mode 100644 index 0000000..c9b6e97 --- /dev/null +++ b/src/utils/JsonValidation.ts @@ -0,0 +1,98 @@ +import { RuntimeException } from '../errors/RuntimeException.js'; +import type { GuildSettingsFile } from '../types/JsonValidationTypes.js'; + +export function validateGuildSettingsFile( + data: unknown, + jsonFilePath: string +): asserts data is GuildSettingsFile { + if (!isRecord(data)) { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: root value must be an object.` + ); + } + + for (const [guildId, guildData] of Object.entries(data)) { + if (!isRecord(guildData)) { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId} must be an object.` + ); + } + + assertExactKeys(guildData, ['channels', 'roles', 'trusted'], `guild ${guildId}`); + + if (!isRecord(guildData.channels)) { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.channels must be an object.` + ); + } + + if (!isRecord(guildData.roles)) { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.roles must be an object.` + ); + } + + assertExactKeys( + guildData.channels, + ['logging', 'commits', 'errors'], + `guild ${guildId}.channels` + ); + assertExactKeys( + guildData.roles, + ['updates', 'qotd', 'support', 'community'], + `guild ${guildId}.roles` + ); + + for (const [key, value] of Object.entries(guildData.channels)) { + if (typeof value !== 'string') { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.channels.${key} must be a string.` + ); + } + } + + for (const [key, value] of Object.entries(guildData.roles)) { + if (typeof value !== 'string') { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.roles.${key} must be a string.` + ); + } + } + + if ( + !Array.isArray(guildData.trusted) || + !guildData.trusted.every((entry) => typeof entry === 'string') + ) { + throw new RuntimeException( + `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.trusted must be an array of strings.` + ); + } + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function assertExactKeys( + value: Record, + expectedKeys: string[], + label: string +): void { + const actualKeys = Object.keys(value).sort(); + const sortedExpectedKeys = [...expectedKeys].sort(); + + if (actualKeys.length !== sortedExpectedKeys.length) { + throw new RuntimeException( + `Invalid guild settings: ${label} must contain exactly ${sortedExpectedKeys.join(', ')}.` + ); + } + + for (let index = 0; index < sortedExpectedKeys.length; index += 1) { + if (actualKeys[index] !== sortedExpectedKeys[index]) { + throw new RuntimeException( + `Invalid guild settings: ${label} must contain exactly ${sortedExpectedKeys.join(', ')}.` + ); + } + } +} From b950143e40f3f428456f0b9df0267765d933158e Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 22:54:58 +0200 Subject: [PATCH 4/6] feature: finalise multi-guild support --- .../{CrashCommand.ts => ShutdownCommand.ts} | 11 +-- src/data/DataStore.ts | 13 ++- src/errors/IllegalArgumentError.ts | 6 ++ .../{RuntimeException.ts => RuntimeError.ts} | 2 +- src/events/ChatBotHandleEvent.ts | 11 ++- src/events/logging/GuildMemberAddEvent.ts | 50 +++++------ src/events/logging/GuildMemberRemoveEvent.ts | 38 +++----- src/structures/Client.ts | 23 ++--- src/utils/ChatBot.ts | 90 ++++++++----------- src/utils/ErrorUtil.ts | 22 +++++ src/utils/GuildMemberLog.ts | 41 +++++++++ src/utils/JsonValidation.ts | 20 ++--- src/utils/Logger.ts | 22 +++++ 13 files changed, 202 insertions(+), 147 deletions(-) rename src/commands/admin/{CrashCommand.ts => ShutdownCommand.ts} (82%) create mode 100644 src/errors/IllegalArgumentError.ts rename src/errors/{RuntimeException.ts => RuntimeError.ts} (67%) create mode 100644 src/utils/ErrorUtil.ts create mode 100644 src/utils/GuildMemberLog.ts diff --git a/src/commands/admin/CrashCommand.ts b/src/commands/admin/ShutdownCommand.ts similarity index 82% rename from src/commands/admin/CrashCommand.ts rename to src/commands/admin/ShutdownCommand.ts index 7a00a9a..152abe3 100644 --- a/src/commands/admin/CrashCommand.ts +++ b/src/commands/admin/ShutdownCommand.ts @@ -4,11 +4,11 @@ import { ExtendedClient } from '@structures/Client.js'; import { Embeds } from '@utils/Embeds.js'; import * as ds from '@data/DataStore.js'; -export default class CrashCommand extends Command { +export default class ShutdownCommand extends Command { constructor() { super({ - name: 'crash', - description: 'Crash the bot', + name: 'shutdown', + description: 'Shut down the bot', requiredPermissions: [PermissionsBitField.Flags.Administrator], checkFlags: CommandCheckFlags.Author | CommandCheckFlags.Guild, }); @@ -33,9 +33,10 @@ export default class CrashCommand extends Command { } await context.reply({ - embeds: [Embeds.error('Crashing...')], + embeds: [Embeds.error('Shutting down...')], }); - process.exit(0); + await client.destroy(); + process.exit(); } } diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts index c3ea1eb..a670f5a 100644 --- a/src/data/DataStore.ts +++ b/src/data/DataStore.ts @@ -4,6 +4,8 @@ import * as fs from 'fs'; import { GUILD_SETTINGS_FILE } from '@data/Config.js'; import { CommandContext } from '@structures/Command.js'; import { validateGuildSettingsFile } from '../utils/JsonValidation.js'; +import { Logger } from '@utils/Logger.js'; +import { IllegalArgumentError } from '../errors/IllegalArgumentError.js'; export class GuildData { readonly guildId: string; @@ -68,6 +70,13 @@ export function getDataFromContext(context: CommandContext): GuildData | undefin return getDataForGuild(context.guild.id); } -export function getDataForGuild(guildId: string): GuildData | undefined { - return dataStore.find((guild) => guild.guildId === guildId); +export function getDataForGuild(guildId: string): GuildData { + const data = dataStore.find((guild) => guild.guildId === guildId); + if (data === undefined) { + throw new IllegalArgumentError( + `Requested data for guild: ${guildId}, but this guild is not found in the guild settings file!` + ); + } + + return data; } diff --git a/src/errors/IllegalArgumentError.ts b/src/errors/IllegalArgumentError.ts new file mode 100644 index 0000000..4ab5da6 --- /dev/null +++ b/src/errors/IllegalArgumentError.ts @@ -0,0 +1,6 @@ +export class IllegalArgumentError extends Error { + constructor(message: string) { + super(message); + this.name = 'IllegalArgumentError'; + } +} diff --git a/src/errors/RuntimeException.ts b/src/errors/RuntimeError.ts similarity index 67% rename from src/errors/RuntimeException.ts rename to src/errors/RuntimeError.ts index 1df8da0..f213b92 100644 --- a/src/errors/RuntimeException.ts +++ b/src/errors/RuntimeError.ts @@ -1,4 +1,4 @@ -export class RuntimeException extends Error { +export class RuntimeError extends Error { constructor(message: string) { super(message); this.name = 'RuntimeException'; diff --git a/src/events/ChatBotHandleEvent.ts b/src/events/ChatBotHandleEvent.ts index 84fc400..51e95aa 100644 --- a/src/events/ChatBotHandleEvent.ts +++ b/src/events/ChatBotHandleEvent.ts @@ -1,6 +1,8 @@ import { Event } from '@structures/Event.js'; import { ExtendedClient } from '@structures/Client.js'; import { Message, PermissionFlagsBits } from 'discord.js'; +import { isErrorWithMessage, messageOrJsonToMessage } from '@utils/ErrorUtil.js'; +import { Logger } from '@utils/Logger.js'; export default class ChatBotHandleEvent extends Event<'messageCreate'> { constructor() { @@ -32,7 +34,14 @@ export default class ChatBotHandleEvent extends Event<'messageCreate'> { await message.channel.sendTyping(); - const res = await client.chatBot.generateResponse(content, message.author); + let res; + try { + res = await client.chatBot.generateResponse(content, message.author); + } catch (error) { + if (isErrorWithMessage(error)) { + await Logger.logErrorWithBot(messageOrJsonToMessage(error.message), message.guild); + } + } if (!res || res.length === 0) { return; diff --git a/src/events/logging/GuildMemberAddEvent.ts b/src/events/logging/GuildMemberAddEvent.ts index fe28afb..1584128 100644 --- a/src/events/logging/GuildMemberAddEvent.ts +++ b/src/events/logging/GuildMemberAddEvent.ts @@ -1,6 +1,10 @@ import { Event } from '@structures/Event.js'; import { ExtendedClient } from '@structures/Client.js'; -import { EmbedBuilder, GuildMember } from 'discord.js'; +import { GuildMember } from 'discord.js'; +import { getDataForGuild, GuildData } from '@data/DataStore.js'; +import { Logger } from '@utils/Logger.js'; +import { isErrorWithMessage } from '@utils/ErrorUtil.js'; +import { buildGuildMemberLogEmbed, sendGuildMemberLogEmbed } from '../../utils/GuildMemberLog.js'; export default class GuildMemberAddEvent extends Event<'guildMemberAdd'> { constructor() { @@ -11,38 +15,26 @@ export default class GuildMemberAddEvent extends Event<'guildMemberAdd'> { public async execute(client: ExtendedClient, member: GuildMember): Promise { const guild = member.guild; + let data: GuildData; - if (guild.id !== Constants.GUILD_ID) { - return; - } - - const loggingChannel = guild.channels.cache.get(Constants.CHANNELS.LOGGING_CHANNEL); + try { + data = getDataForGuild(guild.id); + } catch (error) { + if (isErrorWithMessage(error)) { + Logger.error(error.message); + } - if (!loggingChannel || !loggingChannel.isTextBased()) { return; } - - const avatarUrl = member.user.displayAvatarURL({ size: 256 }); - const createdAt = ``; - const joinedAt = member.joinedAt - ? `` - : 'Unknown'; - - const embed = new EmbedBuilder() - .setTitle('Member Joined') - .setColor(0x57f287) - .setThumbnail(avatarUrl) - .setDescription(`${member.user.toString()} has joined the server.`) - .addFields( - { name: 'Display Name', value: member.displayName || member.user.username, inline: true }, - { name: 'Account Created', value: createdAt, inline: true }, - { name: 'Joined Server', value: joinedAt, inline: false } - ) - .setFooter({ text: `User ID: ${member.user.id}` }) - .setTimestamp(); - - await loggingChannel.send({ embeds: [embed] }); - await member.roles.add(Constants.ROLES.COMMUNITY); + const embed = buildGuildMemberLogEmbed( + member, + 'Member Joined', + 0x57f287, + `${member.user.toString()} has joined the server.` + ); + + await sendGuildMemberLogEmbed(guild, data.channels.logging, embed); + await member.roles.add(data.roles.community); client.updatePresence(); } diff --git a/src/events/logging/GuildMemberRemoveEvent.ts b/src/events/logging/GuildMemberRemoveEvent.ts index 873f4f9..1bd01a0 100644 --- a/src/events/logging/GuildMemberRemoveEvent.ts +++ b/src/events/logging/GuildMemberRemoveEvent.ts @@ -1,6 +1,8 @@ import { Event } from '@structures/Event.js'; import { ExtendedClient } from '@structures/Client.js'; -import { EmbedBuilder, GuildMember } from 'discord.js'; +import { GuildMember } from 'discord.js'; +import { getDataForGuild } from '@data/DataStore.js'; +import { buildGuildMemberLogEmbed, sendGuildMemberLogEmbed } from '../../utils/GuildMemberLog.js'; export default class GuildMemberRemoveEvent extends Event<'guildMemberRemove'> { constructor() { @@ -12,36 +14,20 @@ export default class GuildMemberRemoveEvent extends Event<'guildMemberRemove'> { public async execute(client: ExtendedClient, member: GuildMember): Promise { const guild = member.guild; - if (guild.id !== Constants.GUILD_ID) { - return; - } + const data = getDataForGuild(guild.id); - const loggingChannel = guild.channels.cache.get(Constants.CHANNELS.LOGGING_CHANNEL); - - if (!loggingChannel || !loggingChannel.isTextBased()) { + if (!data) { return; } - const avatarUrl = member.user.displayAvatarURL({ size: 256 }); - const createdAt = ``; - const joinedAt = member.joinedAt - ? `` - : 'Unknown'; - - const embed = new EmbedBuilder() - .setTitle('Member Left') - .setColor(0xed4245) - .setThumbnail(avatarUrl) - .setDescription(`${member.user.toString()} has left the server.`) - .addFields( - { name: 'Display Name', value: member.displayName || member.user.username, inline: true }, - { name: 'Account Created', value: createdAt, inline: true }, - { name: 'Joined Server', value: joinedAt, inline: false } - ) - .setFooter({ text: `User ID: ${member.user.id}` }) - .setTimestamp(); + const embed = buildGuildMemberLogEmbed( + member, + 'Member Left', + 0xed4245, + `${member.user.toString()} has left the server.` + ); - await loggingChannel.send({ embeds: [embed] }); + await sendGuildMemberLogEmbed(guild, data.channels.logging, embed); client.updatePresence(); } diff --git a/src/structures/Client.ts b/src/structures/Client.ts index 0605b1b..d1ba0e7 100644 --- a/src/structures/Client.ts +++ b/src/structures/Client.ts @@ -61,33 +61,20 @@ export class ExtendedClient extends Client { return guild.id == COBALT_GUILD_ID; }); - this.user?.setPresence({ + const presenceName = this.user?.setPresence({ status: 'dnd', activities: [ { - name: `${cobaltGuild?.memberCount} members`, + name: + cobaltGuild?.memberCount === undefined + ? `${cobaltGuild?.memberCount} members` + : 'paint dry', type: ActivityType.Watching, }, ], }); } - public static async logError(message: string, channel: TextChannel): Promise { - try { - if (!channel || !channel.isTextBased()) { - return; - } - - await channel.send({ - embeds: [Embeds.error(message)], - }); - } catch (error) { - Logger.error( - `Failed to send error message: ${error instanceof Error ? error.message : String(error)}` - ); - } - } - private async start(extendedClientOptions: ExtendedClientOptions): Promise { this.chatBot.reset(); diff --git a/src/utils/ChatBot.ts b/src/utils/ChatBot.ts index 8a2fd82..a556076 100644 --- a/src/utils/ChatBot.ts +++ b/src/utils/ChatBot.ts @@ -1,7 +1,6 @@ import { Mistral } from '@mistralai/mistralai'; import { readFileSync } from 'node:fs'; import { ExtendedClient } from '@structures/Client.js'; -import { COBALT_GUILD_ID } from '@data/Config.js'; type ChatAuthor = { id: string; @@ -19,13 +18,13 @@ export class ChatBot { private static readonly MODEL = 'devstral-medium-latest'; private context = ''; - private bot: ExtendedClient; + private client: ExtendedClient; private mistral: Mistral; private messages: ChatMessage[] = []; private turnsSinceReset = 0; constructor(bot: ExtendedClient, apiKey: string) { - this.bot = bot; + this.client = bot; this.mistral = new Mistral({ apiKey }); this.reset(); } @@ -55,60 +54,41 @@ export class ChatBot { this.reset(); } - try { - const trimmedMessage = message.trim().slice(0, ChatBot.MAX_USER_MESSAGE_CHARS); - const requestMessages: ChatMessage[] = [ - ...this.messages, - { - role: 'user', - content: `u:${author.username} id:${author.id} m:${trimmedMessage}`, - }, - ]; - - const response = await this.mistral.chat.complete({ - model: ChatBot.MODEL, - messages: requestMessages, - temperature: 0.2, - topP: 0.7, - maxTokens: 64, - responseFormat: { - type: 'text', - }, - }); + const trimmedMessage = message.trim().slice(0, ChatBot.MAX_USER_MESSAGE_CHARS); + const requestMessages: ChatMessage[] = [ + ...this.messages, + { + role: 'user', + content: `u:${author.username} id:${author.id} m:${trimmedMessage}`, + }, + ]; + + const response = await this.mistral.chat.complete({ + model: ChatBot.MODEL, + messages: requestMessages, + temperature: 0.2, + topP: 0.7, + maxTokens: 64, + responseFormat: { + type: 'text', + }, + }); - const rawResponse = response.choices[0]?.message?.content; - const responseText = ChatBot.extractText(rawResponse); - - this.messages = requestMessages; - - if (responseText) { - this.messages.push({ - role: 'assistant', - content: responseText, - }); - } - - this.turnsSinceReset += 1; - - return responseText ?? 'i errored :/'; - } catch (error: unknown) { - const rawMessage = error instanceof Error ? error.message : String(error); - let cleanMessage = rawMessage; - - if (rawMessage.includes('{')) { - try { - const parsed = JSON.parse(rawMessage.substring(rawMessage.indexOf('{'))) as { - error?: { message?: string }; - }; - cleanMessage = parsed.error?.message || 'Quota Exceeded/API Error'; - } catch { - cleanMessage = rawMessage.split('\n')[0]; - } - } - - await this.bot.logError(cleanMessage); - return `i errored :/ (see <#${Constants.CHANNELS.BOT_ERRORS}>)`; + const rawResponse = response.choices[0]?.message?.content; + const responseText = ChatBot.extractText(rawResponse); + + this.messages = requestMessages; + + if (responseText) { + this.messages.push({ + role: 'assistant', + content: responseText, + }); } + + this.turnsSinceReset += 1; + + return responseText ?? 'i errored :/'; } private static extractText( diff --git a/src/utils/ErrorUtil.ts b/src/utils/ErrorUtil.ts new file mode 100644 index 0000000..a55d6e7 --- /dev/null +++ b/src/utils/ErrorUtil.ts @@ -0,0 +1,22 @@ +export function isErrorWithMessage(error: unknown): error is { message: string } { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +} + +export function messageOrJsonToMessage(errorMessage: string): string { + let message: string = errorMessage; + try { + const parsed = JSON.parse(message.substring(message.indexOf('{'))) as { + error?: { message?: string }; + }; + message = parsed.error?.message || 'Quota Exceeded/API Error'; + } catch { + message = message.split('\n')[0]; + } + + return message; +} diff --git a/src/utils/GuildMemberLog.ts b/src/utils/GuildMemberLog.ts new file mode 100644 index 0000000..0735196 --- /dev/null +++ b/src/utils/GuildMemberLog.ts @@ -0,0 +1,41 @@ +import { EmbedBuilder, Guild, GuildMember } from 'discord.js'; + +export function buildGuildMemberLogEmbed( + member: GuildMember, + title: string, + color: number, + description: string +): EmbedBuilder { + const avatarUrl = member.user.displayAvatarURL({ size: 256 }); + const createdAt = ``; + const joinedAt = member.joinedAt + ? `` + : 'Unknown'; + + return new EmbedBuilder() + .setTitle(title) + .setColor(color) + .setThumbnail(avatarUrl) + .setDescription(description) + .addFields( + { name: 'Display Name', value: member.displayName || member.user.username, inline: true }, + { name: 'Account Created', value: createdAt, inline: true }, + { name: 'Joined Server', value: joinedAt, inline: false } + ) + .setFooter({ text: `User ID: ${member.user.id}` }) + .setTimestamp(); +} + +export async function sendGuildMemberLogEmbed( + guild: Guild, + channelId: string, + embed: EmbedBuilder +): Promise { + const channel = guild.channels.cache.get(channelId); + + if (!channel || !channel.isTextBased()) { + return; + } + + await channel.send({ embeds: [embed] }); +} diff --git a/src/utils/JsonValidation.ts b/src/utils/JsonValidation.ts index c9b6e97..638624b 100644 --- a/src/utils/JsonValidation.ts +++ b/src/utils/JsonValidation.ts @@ -1,4 +1,4 @@ -import { RuntimeException } from '../errors/RuntimeException.js'; +import { RuntimeError } from '../errors/RuntimeError.js'; import type { GuildSettingsFile } from '../types/JsonValidationTypes.js'; export function validateGuildSettingsFile( @@ -6,14 +6,14 @@ export function validateGuildSettingsFile( jsonFilePath: string ): asserts data is GuildSettingsFile { if (!isRecord(data)) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: root value must be an object.` ); } for (const [guildId, guildData] of Object.entries(data)) { if (!isRecord(guildData)) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId} must be an object.` ); } @@ -21,13 +21,13 @@ export function validateGuildSettingsFile( assertExactKeys(guildData, ['channels', 'roles', 'trusted'], `guild ${guildId}`); if (!isRecord(guildData.channels)) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.channels must be an object.` ); } if (!isRecord(guildData.roles)) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.roles must be an object.` ); } @@ -45,7 +45,7 @@ export function validateGuildSettingsFile( for (const [key, value] of Object.entries(guildData.channels)) { if (typeof value !== 'string') { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.channels.${key} must be a string.` ); } @@ -53,7 +53,7 @@ export function validateGuildSettingsFile( for (const [key, value] of Object.entries(guildData.roles)) { if (typeof value !== 'string') { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.roles.${key} must be a string.` ); } @@ -63,7 +63,7 @@ export function validateGuildSettingsFile( !Array.isArray(guildData.trusted) || !guildData.trusted.every((entry) => typeof entry === 'string') ) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings in ${jsonFilePath}: guild ${guildId}.trusted must be an array of strings.` ); } @@ -83,14 +83,14 @@ function assertExactKeys( const sortedExpectedKeys = [...expectedKeys].sort(); if (actualKeys.length !== sortedExpectedKeys.length) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings: ${label} must contain exactly ${sortedExpectedKeys.join(', ')}.` ); } for (let index = 0; index < sortedExpectedKeys.length; index += 1) { if (actualKeys[index] !== sortedExpectedKeys[index]) { - throw new RuntimeException( + throw new RuntimeError( `Invalid guild settings: ${label} must contain exactly ${sortedExpectedKeys.join(', ')}.` ); } diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index deefe8e..509089a 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,4 +1,8 @@ import chalk from 'chalk'; +import { Client, Guild, TextChannel } from 'discord.js'; +import { Embeds } from './Embeds.js'; +import { getDataForGuild } from '@data/DataStore.js'; +import { ExtendedClient } from '@structures/Client.js'; export class Logger { public static success(message: string): void { @@ -29,4 +33,22 @@ export class Logger { .map((v) => String(v).padStart(2, '0')) .join(':'); } + + public static async logErrorWithBot(message: string, guild: Guild): Promise { + const channel = await guild.channels.fetch(getDataForGuild(guild.id).channels.errors); + + try { + if (!channel || !channel.isTextBased()) { + return; + } + + await channel.send({ + embeds: [Embeds.error(message)], + }); + } catch (error) { + Logger.error( + `Failed to send error message: ${error instanceof Error ? error.message : String(error)}` + ); + } + } } From 4e36ea2105ea2ccb78a4dc1aa3945dad202e8e8b Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 23:12:53 +0200 Subject: [PATCH 5/6] feat: update presence activity --- .gitignore | 8 +++++--- src/structures/Client.ts | 34 +++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index a0d218e..8d28939 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -node_modules -dist -.env \ No newline at end of file +node_modules/ +dist/ +.env + +.idea/ diff --git a/src/structures/Client.ts b/src/structures/Client.ts index d1ba0e7..f1fefe2 100644 --- a/src/structures/Client.ts +++ b/src/structures/Client.ts @@ -1,4 +1,11 @@ -import { ActivityType, Client, GatewayIntentBits, Partials, TextChannel } from 'discord.js'; +import { + ActivityOptions, + ActivityType, + Client, + GatewayIntentBits, + Partials, + TextChannel, +} from 'discord.js'; import { readdir } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -61,17 +68,22 @@ export class ExtendedClient extends Client { return guild.id == COBALT_GUILD_ID; }); - const presenceName = this.user?.setPresence({ + let activity: ActivityOptions = { + name: "Sniffing glue", + type: ActivityType.Custom, + state: "Sniffing glue", + }; + + if (cobaltGuild?.memberCount !== undefined) { + activity = { + name: `${cobaltGuild?.memberCount} members`, + type: ActivityType.Watching, + } + } + + this.user?.setPresence({ status: 'dnd', - activities: [ - { - name: - cobaltGuild?.memberCount === undefined - ? `${cobaltGuild?.memberCount} members` - : 'paint dry', - type: ActivityType.Watching, - }, - ], + activities: [activity], }); } From e85b53ce67c193eba5c3bfd1e94f419bc97f1e72 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 11 Apr 2026 23:14:50 +0200 Subject: [PATCH 6/6] chore: run prettier --- src/structures/Client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structures/Client.ts b/src/structures/Client.ts index f1fefe2..9e6a932 100644 --- a/src/structures/Client.ts +++ b/src/structures/Client.ts @@ -69,16 +69,16 @@ export class ExtendedClient extends Client { }); let activity: ActivityOptions = { - name: "Sniffing glue", + name: 'Sniffing glue', type: ActivityType.Custom, - state: "Sniffing glue", + state: 'Sniffing glue', }; if (cobaltGuild?.memberCount !== undefined) { activity = { name: `${cobaltGuild?.memberCount} members`, type: ActivityType.Watching, - } + }; } this.user?.setPresence({