feat(autorole): implement multi-role UX, persistence and automatic toggle logic
This commit is contained in:
parent
c71d263607
commit
f3f882cd06
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `botRoleId` on the `AutoRoleConfig` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `userRoleId` on the `AutoRoleConfig` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "AutoRoleConfig" DROP COLUMN "botRoleId",
|
||||
DROP COLUMN "userRoleId",
|
||||
ADD COLUMN "botRoleIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "userRoleIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
|
@ -210,8 +210,8 @@ model FeverState {
|
|||
|
||||
model AutoRoleConfig {
|
||||
guildId String @id
|
||||
userRoleId String?
|
||||
botRoleId String?
|
||||
userRoleIds String[] @default([])
|
||||
botRoleIds String[] @default([])
|
||||
isEnabled Boolean @default(false)
|
||||
botEnabled Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
|
|
|||
|
|
@ -29,75 +29,59 @@ class AutoRoleCommand extends Command {
|
|||
}
|
||||
|
||||
protected override async handle(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const guild = interaction.guild!;
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(t(locale, 'commands.autorole.statusTitle'))
|
||||
.setColor(Colors.Blue)
|
||||
.addFields(
|
||||
{
|
||||
name: t(locale, 'commands.autorole.userRoleLabel'),
|
||||
value: config?.userRoleId ? `<@&${config.userRoleId}>` : t(locale, 'commands.autorole.notSet'),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: t(locale, 'commands.autorole.botRoleLabel'),
|
||||
value: config?.botRoleId ? `<@&${config.botRoleId}>` : t(locale, 'commands.autorole.notSet'),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: t(locale, 'commands.autorole.statusLabel'),
|
||||
value: config?.isEnabled ? `✅ ${t(locale, 'commands.autorole.enabled')}` : `❌ ${t(locale, 'commands.autorole.disabled')}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: t(locale, 'commands.autorole.botStatusLabel'),
|
||||
value: config?.botEnabled ? `✅ ${t(locale, 'commands.autorole.enabled')}` : `❌ ${t(locale, 'commands.autorole.disabled')}`,
|
||||
inline: true
|
||||
}
|
||||
);
|
||||
|
||||
const row1 = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_toggle')
|
||||
.setLabel(t(locale, 'commands.autorole.statusLabel'))
|
||||
.setStyle(config?.isEnabled ? ButtonStyle.Danger : ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_toggle_bot')
|
||||
.setLabel(t(locale, 'commands.autorole.botStatusLabel'))
|
||||
.setStyle(config?.botEnabled ? ButtonStyle.Danger : ButtonStyle.Success)
|
||||
);
|
||||
|
||||
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_set_user')
|
||||
.setLabel(t(locale, 'commands.autorole.userRoleLabel'))
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_set_bot')
|
||||
.setLabel(t(locale, 'commands.autorole.botRoleLabel'))
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
const row3 = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_retroactive')
|
||||
.setLabel(t(locale, 'commands.autorole.retroactiveBtn'))
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(!config?.userRoleId),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_exclude')
|
||||
.setLabel(t(locale, 'commands.autorole.excludeTitle'))
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
components: [row1, row2, row3],
|
||||
ephemeral: true,
|
||||
const dashboard = await generateAutoRoleDashboard(guild, locale);
|
||||
await interaction.editReply({
|
||||
...dashboard
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateAutoRoleDashboard(guild: import('discord.js').Guild, locale: SupportedLocale) {
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(t(locale, 'commands.autorole.statusTitle'))
|
||||
.setColor(Colors.Blue)
|
||||
.setDescription(t(locale, 'commands.autorole.description') || '유저 및 봇이 서버에 접속할 때 자동으로 부여할 기본 역할을 선택하세요. 역할을 선택하면 즉시 활성화됩니다.');
|
||||
|
||||
const userSelect = new RoleSelectMenuBuilder()
|
||||
.setCustomId('autorole_select_user')
|
||||
.setPlaceholder(t(locale, 'commands.autorole.userRolePlaceholder'))
|
||||
.setMaxValues(10);
|
||||
if (config?.userRoleIds && config.userRoleIds.length > 0) {
|
||||
userSelect.addDefaultRoles(config.userRoleIds);
|
||||
}
|
||||
|
||||
const rowUserRole = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(userSelect);
|
||||
|
||||
const botSelect = new RoleSelectMenuBuilder()
|
||||
.setCustomId('autorole_select_bot')
|
||||
.setPlaceholder(t(locale, 'commands.autorole.botRolePlaceholder'))
|
||||
.setMaxValues(10);
|
||||
if (config?.botRoleIds && config.botRoleIds.length > 0) {
|
||||
botSelect.addDefaultRoles(config.botRoleIds);
|
||||
}
|
||||
|
||||
const rowBotRole = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(botSelect);
|
||||
|
||||
const rowRetroactive = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_retroactive')
|
||||
.setLabel(t(locale, 'commands.autorole.retroactiveBtn'))
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(!config?.userRoleIds || config.userRoleIds.length === 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('autorole_exclude')
|
||||
.setLabel(t(locale, 'commands.autorole.excludeTitle'))
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
return {
|
||||
embeds: [embed],
|
||||
components: [rowUserRole, rowBotRole, rowRetroactive]
|
||||
};
|
||||
}
|
||||
|
||||
export default new AutoRoleCommand().toModule();
|
||||
|
|
|
|||
|
|
@ -151,32 +151,14 @@ export default {
|
|||
await interaction.deferUpdate();
|
||||
|
||||
const guild = interaction.guild!;
|
||||
if (interaction.customId === 'autorole_toggle') {
|
||||
if (interaction.customId === 'autorole_retroactive') {
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
const newEnabled = !config?.isEnabled;
|
||||
await autoRoleService.setEnabled(guild.id, newEnabled);
|
||||
await interaction.editReply({ content: t(locale, 'commands.autorole.updateSuccess'), embeds: [], components: [] });
|
||||
} else if (interaction.customId === 'autorole_toggle_bot') {
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
const newBotEnabled = !config?.botEnabled;
|
||||
await autoRoleService.updateConfig(guild.id, { botEnabled: newBotEnabled });
|
||||
await interaction.editReply({ content: t(locale, 'commands.autorole.updateSuccess'), embeds: [], components: [] });
|
||||
} else if (interaction.customId === 'autorole_retroactive') {
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
if (config?.userRoleId) {
|
||||
await autoRoleService.applyRetroactively(guild, config.userRoleId, interaction.user.id);
|
||||
await interaction.editReply({ content: t(locale, 'commands.autorole.retroactiveStarted'), embeds: [], components: [] });
|
||||
if (config?.userRoleIds && config.userRoleIds.length > 0) {
|
||||
await autoRoleService.applyRetroactively(guild, config.userRoleIds, interaction.user.id);
|
||||
const { generateAutoRoleDashboard } = require('../commands/autorole');
|
||||
const dashboard = await generateAutoRoleDashboard(guild, locale);
|
||||
await interaction.editReply({ content: t(locale, 'commands.autorole.retroactiveStarted'), ...dashboard });
|
||||
}
|
||||
} else if (interaction.customId === 'autorole_set_user' || interaction.customId === 'autorole_set_bot') {
|
||||
const isBot = interaction.customId === 'autorole_set_bot';
|
||||
const { RoleSelectMenuBuilder } = require('discord.js');
|
||||
const roleSelect = new RoleSelectMenuBuilder()
|
||||
.setCustomId(`autorole_select_${isBot ? 'bot' : 'user'}`)
|
||||
.setPlaceholder(isBot ? t(locale, 'commands.autorole.botRoleLabel') : t(locale, 'commands.autorole.userRoleLabel'))
|
||||
.setMaxValues(1);
|
||||
|
||||
const selectRow = new ActionRowBuilder<any>().addComponents(roleSelect);
|
||||
await interaction.editReply({ components: [selectRow] });
|
||||
}
|
||||
// 나머지 버튼 처리
|
||||
}, locale);
|
||||
|
|
@ -190,16 +172,18 @@ export default {
|
|||
|
||||
const guild = interaction.guild!;
|
||||
const isBot = interaction.customId.includes('bot');
|
||||
const roleId = interaction.values[0];
|
||||
const roleIds = interaction.values;
|
||||
|
||||
await autoRoleService.updateConfig(guild.id, {
|
||||
[isBot ? 'botRoleId' : 'userRoleId']: roleId
|
||||
[isBot ? 'botRoleIds' : 'userRoleIds']: roleIds
|
||||
});
|
||||
|
||||
const { generateAutoRoleDashboard } = require('../commands/autorole');
|
||||
const dashboard = await generateAutoRoleDashboard(guild, locale);
|
||||
|
||||
await interaction.editReply({
|
||||
content: t(locale, 'commands.autorole.updateSuccess'),
|
||||
embeds: [],
|
||||
components: []
|
||||
content: '',
|
||||
...dashboard
|
||||
});
|
||||
}, locale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,6 +198,12 @@ export const en: TranslationSchema = {
|
|||
botRoleLabel: 'Bot Role',
|
||||
statusLabel: 'User Auto Assignment',
|
||||
botStatusLabel: 'Bot Auto Assignment',
|
||||
userRolePlaceholder: 'Select default user role',
|
||||
botRolePlaceholder: 'Select default bot role',
|
||||
toggleUserEnable: '🟢 Enable User AutoRole',
|
||||
toggleUserDisable: '🔴 Disable User AutoRole',
|
||||
toggleBotEnable: '🟢 Enable Bot AutoRole',
|
||||
toggleBotDisable: '🔴 Disable Bot AutoRole',
|
||||
notSet: 'Not Set',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
|
|
|
|||
|
|
@ -198,6 +198,12 @@ export const ko: TranslationSchema = {
|
|||
botRoleLabel: '봇 역할',
|
||||
statusLabel: '유저 자동 부여',
|
||||
botStatusLabel: '봇 자동 부여',
|
||||
userRolePlaceholder: '유저 기본 역할을 선택하세요',
|
||||
botRolePlaceholder: '봇 기본 역할을 선택하세요',
|
||||
toggleUserEnable: '🟢 유저 자동부여 켜기',
|
||||
toggleUserDisable: '🔴 유저 자동부여 끄기',
|
||||
toggleBotEnable: '🟢 봇 자동부여 켜기',
|
||||
toggleBotDisable: '🔴 봇 자동부여 끄기',
|
||||
notSet: '미설정',
|
||||
enabled: '활성',
|
||||
disabled: '비활성',
|
||||
|
|
|
|||
|
|
@ -152,6 +152,12 @@ export interface TranslationSchema {
|
|||
botRoleLabel: string;
|
||||
statusLabel: string;
|
||||
botStatusLabel: string;
|
||||
userRolePlaceholder: string;
|
||||
botRolePlaceholder: string;
|
||||
toggleUserEnable: string;
|
||||
toggleUserDisable: string;
|
||||
toggleBotEnable: string;
|
||||
toggleBotDisable: string;
|
||||
notSet: string;
|
||||
enabled: string;
|
||||
disabled: string;
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export class AutoRoleService {
|
|||
* 서버의 자동 역할 설정을 업데이트합니다.
|
||||
*/
|
||||
async updateConfig(guildId: string, data: {
|
||||
userRoleId?: string | null;
|
||||
botRoleId?: string | null;
|
||||
userRoleIds?: string[];
|
||||
botRoleIds?: string[];
|
||||
isEnabled?: boolean;
|
||||
botEnabled?: boolean;
|
||||
}) {
|
||||
|
|
@ -89,12 +89,12 @@ export class AutoRoleService {
|
|||
const excludes = await this.getExcludes(member.guild.id);
|
||||
|
||||
// 유저 ID 체크
|
||||
if (excludes.some(e => e.type === 'USER' && e.targetId === member.id)) {
|
||||
if (excludes.some((e: any) => e.type === 'USER' && e.targetId === member.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 역할 ID 체크
|
||||
if (excludes.some(e => e.type === 'ROLE' && member.roles.cache.has(e.targetId))) {
|
||||
if (excludes.some((e: any) => e.type === 'ROLE' && member.roles.cache.has(e.targetId))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -104,22 +104,30 @@ export class AutoRoleService {
|
|||
/**
|
||||
* 서버 전체에 역할을 소급 적용합니다 (백그라운드 처리).
|
||||
*/
|
||||
async applyRetroactively(guild: Guild, roleId: string, initiatorId: string) {
|
||||
const role = guild.roles.cache.get(roleId);
|
||||
if (!role) return;
|
||||
async applyRetroactively(guild: Guild, roleIds: string[], initiatorId: string) {
|
||||
if (!roleIds || roleIds.length === 0) return;
|
||||
|
||||
const roles = roleIds.map(id => guild.roles.cache.get(id)).filter((r): r is import('discord.js').Role => r !== undefined);
|
||||
if (roles.length === 0) return;
|
||||
|
||||
// 봇의 권한 및 순위 확인
|
||||
const botMember = guild.members.me;
|
||||
if (!botMember || !botMember.permissions.has(PermissionFlagsBits.ManageRoles) || botMember.roles.highest.position <= role.position) {
|
||||
logger.warn(`AutoRole: Cannot apply role ${role.name} in guild ${guild.id} due to hierarchy/permissions.`);
|
||||
if (!botMember || !botMember.permissions.has(PermissionFlagsBits.ManageRoles)) {
|
||||
logger.warn(`AutoRole: Cannot apply roles in guild ${guild.id} due to missing ManageRoles permission.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const validRoles = roles.filter(r => botMember.roles.highest.position > r.position);
|
||||
if (validRoles.length === 0) {
|
||||
logger.warn(`AutoRole: Cannot apply roles in guild ${guild.id} due to hierarchy limitations.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 멤버 페치 (캐시되지 않았을 수 있음)
|
||||
const members = await guild.members.fetch();
|
||||
const targets = members.filter(m => !m.user.bot && !m.roles.cache.has(roleId));
|
||||
const targets = members.filter(m => !m.user.bot && validRoles.some(r => !m.roles.cache.has(r.id)));
|
||||
|
||||
logger.info(`AutoRole: Starting retroactive application of role ${role.name} to ${targets.size} members in guild ${guild.id}`);
|
||||
logger.info(`AutoRole: Starting retroactive application of ${validRoles.length} roles to ${targets.size} members in guild ${guild.id}`);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
|
@ -130,7 +138,7 @@ export class AutoRoleService {
|
|||
try {
|
||||
if (await this.isExcluded(member)) continue;
|
||||
|
||||
await member.roles.add(role);
|
||||
await member.roles.add(validRoles);
|
||||
successCount++;
|
||||
|
||||
// Rate Limit 방지를 위한 지연 (1.5초)
|
||||
|
|
@ -154,7 +162,7 @@ export class AutoRoleService {
|
|||
category: 'SYSTEM',
|
||||
severity: 'INFO',
|
||||
title: 'Retroactive Role Assignment Completed',
|
||||
description: `Retroactive assignment of <@&${roleId}> role by <@${initiatorId}> has finished.\n- Success: ${successCount}\n- Failed: ${failCount}`,
|
||||
description: `Retroactive assignment of roles (${validRoles.map(r => `<@&${r.id}>`).join(', ')}) by <@${initiatorId}> has finished.\n- Success: ${successCount}\n- Failed: ${failCount}`,
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ export class InviteService {
|
|||
|
||||
// 1. 기본 오토롤 확인
|
||||
const autoRoleConfig = await autoRoleService.getConfig(guild.id);
|
||||
if (autoRoleConfig?.isEnabled) {
|
||||
const roleId = member.user.bot ? autoRoleConfig.botRoleId : autoRoleConfig.userRoleId;
|
||||
if (roleId && (member.user.bot ? autoRoleConfig.botEnabled : true)) {
|
||||
rolesToGive.push(roleId);
|
||||
if (autoRoleConfig) {
|
||||
const roleIds = member.user.bot ? autoRoleConfig.botRoleIds : autoRoleConfig.userRoleIds;
|
||||
if (roleIds && roleIds.length > 0) {
|
||||
rolesToGive.push(...roleIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue