feat(bots/discord)!: add admin config

This commit is contained in:
PalmDevs
2024-07-26 00:53:25 +07:00
parent e86180fe29
commit d0acab1915
12 changed files with 46 additions and 38 deletions

View File

@@ -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: {

View File

@@ -1,7 +1,10 @@
import type { BaseMessageOptions } from 'discord.js'
export type Config = {
owners: string[]
admin?: {
users?: string[]
roles?: Record<string, string[]>
}
guilds: string[]
moderation?: {
roles: string[]

View File

@@ -12,7 +12,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute(_, interaction) {

View File

@@ -25,7 +25,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute(_, interaction) {

View File

@@ -11,7 +11,7 @@ export default {
.setDMPermission(true)
.toJSON(),
ownerOnly: true,
adminOnly: true,
global: true,
async execute({ api, logger }, interaction) {

View File

@@ -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.',

View File

@@ -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.',

View File

@@ -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
}

View File

@@ -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']({

View File

@@ -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() &&

View File

@@ -1,19 +0,0 @@
import type { Command } from '$commands/types'
import { listAllFilesRecursive } from '$utils/fs'
export const loadCommands = async (dir: string) => {
const commandsMap: Record<string, Command> = {}
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
}

View File

@@ -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)))
}