diff --git a/bots/discord/config.js b/bots/discord/config.js index 16d79b4..24d919d 100644 --- a/bots/discord/config.js +++ b/bots/discord/config.js @@ -4,7 +4,18 @@ * @type {import('./config.schema').Config} */ export default { - owners: ['USER_ID_HERE'], + /** + * ? ADMIN CONFIGURATION + * Bot administrators can run destructive commands like /stop, or /register. + * + * ! The match condition is `any`: If the user ID matches or the member has a specific role in the list, it considers that user as admin. + */ + admin: { + users: ['USER_ID_HERE'], + roles: { + GUILD_ID_HERE: ['ROLE_ID_HERE'], + }, + }, guilds: ['GUILD_ID_HERE'], moderation: { cure: { diff --git a/bots/discord/config.schema.ts b/bots/discord/config.schema.ts index 23b2fb9..4ca9d3b 100644 --- a/bots/discord/config.schema.ts +++ b/bots/discord/config.schema.ts @@ -1,7 +1,10 @@ import type { BaseMessageOptions } from 'discord.js' export type Config = { - owners: string[] + admin?: { + users?: string[] + roles?: Record + } guilds: string[] moderation?: { roles: string[] diff --git a/bots/discord/src/commands/development/eval.ts b/bots/discord/src/commands/development/eval.ts index 223d66d..59a9570 100644 --- a/bots/discord/src/commands/development/eval.ts +++ b/bots/discord/src/commands/development/eval.ts @@ -12,7 +12,7 @@ export default { .setDMPermission(true) .toJSON(), - ownerOnly: true, + adminOnly: true, global: true, async execute(_, interaction) { diff --git a/bots/discord/src/commands/development/exception-test.ts b/bots/discord/src/commands/development/exception-test.ts index 70dfbae..cde89ae 100644 --- a/bots/discord/src/commands/development/exception-test.ts +++ b/bots/discord/src/commands/development/exception-test.ts @@ -25,7 +25,7 @@ export default { .setDMPermission(true) .toJSON(), - ownerOnly: true, + adminOnly: true, global: true, async execute(_, interaction) { diff --git a/bots/discord/src/commands/development/stop.ts b/bots/discord/src/commands/development/stop.ts index 2d08763..3e7bbf4 100644 --- a/bots/discord/src/commands/development/stop.ts +++ b/bots/discord/src/commands/development/stop.ts @@ -11,7 +11,7 @@ export default { .setDMPermission(true) .toJSON(), - ownerOnly: true, + adminOnly: true, global: true, async execute({ api, logger }, interaction) { diff --git a/bots/discord/src/commands/moderation/mute.ts b/bots/discord/src/commands/moderation/mute.ts index fededd2..aeba84d 100644 --- a/bots/discord/src/commands/moderation/mute.ts +++ b/bots/discord/src/commands/moderation/mute.ts @@ -24,7 +24,7 @@ export default { global: false, - async execute({ logger }, interaction, { userIsOwner }) { + async execute({ logger }, interaction, { isExecutorBotAdmin: isExecutorAdmin }) { const user = interaction.options.getUser('member', true) const reason = interaction.options.getString('reason') ?? 'No reason provided' const duration = interaction.options.getString('duration') @@ -48,7 +48,7 @@ export default { if (!member.manageable) throw new CommandError(CommandErrorType.Generic, 'This user cannot be managed by the bot.') - if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !userIsOwner) + if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !isExecutorAdmin) throw new CommandError( CommandErrorType.InvalidUser, 'You cannot mute a user with a role equal to or higher than yours.', diff --git a/bots/discord/src/commands/moderation/role-preset.ts b/bots/discord/src/commands/moderation/role-preset.ts index 0d4cbff..75457f1 100644 --- a/bots/discord/src/commands/moderation/role-preset.ts +++ b/bots/discord/src/commands/moderation/role-preset.ts @@ -35,7 +35,7 @@ export default { global: false, - async execute({ logger }, interaction, { userIsOwner }) { + async execute({ logger }, interaction, { isExecutorBotAdmin: isExecutorAdmin }) { const action = interaction.options.getString('action', true) as 'apply' | 'remove' const user = interaction.options.getUser('member', true) const preset = interaction.options.getString('preset', true) @@ -61,7 +61,7 @@ export default { 'The duration must be at least 1 millisecond long.', ) - if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !userIsOwner) + if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0 && !isExecutorAdmin) throw new CommandError( CommandErrorType.InvalidUser, 'You cannot apply a role preset to a user with a role equal to or higher than yours.', diff --git a/bots/discord/src/commands/types.ts b/bots/discord/src/commands/types.ts index e782811..ceb232f 100644 --- a/bots/discord/src/commands/types.ts +++ b/bots/discord/src/commands/types.ts @@ -39,10 +39,10 @@ export type Command = { roles?: string[] } /** - * Whether this command can only be used by bot owners. + * Whether this command can only be used by bot admins. * @default false */ - ownerOnly?: boolean + adminOnly?: boolean /** * Whether to register this command as a global slash command. * This is set to `false` and commands will be registered in allowed guilds only by default. @@ -52,5 +52,5 @@ export type Command = { } export interface Info { - userIsOwner: boolean + isExecutorBotAdmin: boolean } diff --git a/bots/discord/src/events/discord/interactionCreate/chatCommand.ts b/bots/discord/src/events/discord/interactionCreate/chatCommand.ts index 0972b16..b608dd8 100644 --- a/bots/discord/src/events/discord/interactionCreate/chatCommand.ts +++ b/bots/discord/src/events/discord/interactionCreate/chatCommand.ts @@ -1,4 +1,5 @@ import CommandError from '$/classes/CommandError' +import { isAdmin } from '$/utils/discord/permissions' import { createErrorEmbed, createStackTraceEmbed } from '$utils/discord/embeds' import { on, withContext } from '$utils/discord/events' @@ -11,14 +12,14 @@ withContext(on, 'interactionCreate', async (context, interaction) => { logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag}`) if (!command) return void logger.error(`Command ${interaction.commandName} not implemented but registered!!!`) - const isOwner = config.owners.includes(interaction.user.id) + const isExecutorBotAdmin = isAdmin(await interaction.guild?.members.fetch(interaction.user.id) || interaction.user, config.admin) /** - * Owner check + * Admin check */ - if (command.ownerOnly && !isOwner) + if (command.adminOnly && !isExecutorBotAdmin) return void (await interaction.reply({ - embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot owners.')], + embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot admins.')], ephemeral: true, })) @@ -39,7 +40,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => { */ if (interaction.inGuild()) { // Bot owners get bypass - if (command.memberRequirements && !isOwner) { + if (command.memberRequirements && !isExecutorBotAdmin) { const { permissions = 0n, roles = [], mode } = command.memberRequirements const member = await interaction.guild!.members.fetch(interaction.user.id) @@ -69,7 +70,7 @@ withContext(on, 'interactionCreate', async (context, interaction) => { try { logger.debug(`Command ${interaction.commandName} being executed`) - await command.execute(context, interaction, { userIsOwner: isOwner }) + await command.execute(context, interaction, { isExecutorBotAdmin }) } catch (err) { logger.error(`Error while executing command ${interaction.commandName}:`, err) await interaction[interaction.replied ? 'followUp' : 'reply']({ diff --git a/bots/discord/src/events/discord/messageReactionAdd/correctResponse.ts b/bots/discord/src/events/discord/messageReactionAdd/correctResponse.ts index 74a700d..ccc6fe8 100644 --- a/bots/discord/src/events/discord/messageReactionAdd/correctResponse.ts +++ b/bots/discord/src/events/discord/messageReactionAdd/correctResponse.ts @@ -14,6 +14,7 @@ import type { ConfigMessageScanResponseLabelConfig } from '$/../config.schema' import { responses } from '$/database/schemas' import { handleUserResponseCorrection } from '$/utils/discord/messageScan' import { eq } from 'drizzle-orm' +import { isAdmin } from '$/utils/discord/permissions' const PossibleReactions = Object.values(Reactions) as string[] @@ -32,7 +33,7 @@ withContext(on, 'messageReactionAdd', async (context, rct, user) => { if (reactionMessage.author.id !== reaction.client.user!.id) return if (!PossibleReactions.includes(reaction.emoji.name!)) return - if (!config.owners.includes(user.id)) { + if (!isAdmin(reactionMessage.member || reactionMessage.author, config.admin)) { // User is in guild, and config has member requirements if ( reactionMessage.inGuild() && diff --git a/bots/discord/src/utils/discord/commands.ts b/bots/discord/src/utils/discord/commands.ts deleted file mode 100644 index 188946f..0000000 --- a/bots/discord/src/utils/discord/commands.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Command } from '$commands/types' -import { listAllFilesRecursive } from '$utils/fs' - -export const loadCommands = async (dir: string) => { - const commandsMap: Record = {} - const files = listAllFilesRecursive(dir) - const commands = await Promise.all( - files.map(async file => { - const command = await import(file) - return command.default - }), - ) - - for (const command of commands) { - if (command) commandsMap[command.data.name] = command - } - - return commandsMap -} diff --git a/bots/discord/src/utils/discord/permissions.ts b/bots/discord/src/utils/discord/permissions.ts new file mode 100644 index 0000000..6bb2540 --- /dev/null +++ b/bots/discord/src/utils/discord/permissions.ts @@ -0,0 +1,11 @@ +import { GuildMember, type User } from 'discord.js' +import type { Config } from 'config.schema' + +export const isAdmin = (userOrMember: User | GuildMember, adminConfig: Config['admin']) => { + return adminConfig?.users?.includes(userOrMember.id) || (userOrMember instanceof GuildMember && isMemberAdmin(userOrMember, adminConfig)) +} + +export const isMemberAdmin = (member: GuildMember, adminConfig: Config['admin']) => { + const roles = new Set(member.roles.cache.keys()) + return Boolean(adminConfig?.roles?.[member.guild.id]?.some(role => roles.has(role))) +} \ No newline at end of file