import { Guild, GuildMember, PermissionFlagsBits } from 'discord.js'; import { prisma } from '../database'; import { logger } from '../utils/logger'; import { auditLogService } from './AuditLogService'; export class AutoRoleService { /** * 서버의 자동 역할 설정을 조회합니다. */ async getConfig(guildId: string) { return prisma.autoRoleConfig.findUnique({ where: { guildId }, }); } /** * 서버의 자동 역할 설정을 업데이트합니다. */ async updateConfig(guildId: string, data: { userRoleId?: string | null; botRoleId?: string | null; isEnabled?: boolean; botEnabled?: boolean; }) { return prisma.autoRoleConfig.upsert({ where: { guildId }, create: { guildId, ...data, }, update: data, }); } /** * 자동 역할 부여 기능을 활성/비활성합니다. */ async setEnabled(guildId: string, enabled: boolean) { return this.updateConfig(guildId, { isEnabled: enabled }); } /** * 소급 적용 제외 대상을 추가합니다. */ async addExclude(guildId: string, targetId: string, type: 'ROLE' | 'USER') { return prisma.autoRoleExclude.upsert({ where: { guildId_targetId_type: { guildId, targetId, type, }, }, create: { guildId, targetId, type, }, update: {}, }); } /** * 소급 적용 제외 대상을 제거합니다. */ async removeExclude(guildId: string, targetId: string, type: 'ROLE' | 'USER') { return prisma.autoRoleExclude.deleteMany({ where: { guildId, targetId, type, }, }); } /** * 소급 적용 제외 대상 목록을 조회합니다. */ async getExcludes(guildId: string) { return prisma.autoRoleExclude.findMany({ where: { guildId }, }); } /** * 특정 멤버가 소급 적용 제외 대상인지 확인합니다. */ async isExcluded(member: GuildMember): Promise { const excludes = await this.getExcludes(member.guild.id); // 유저 ID 체크 if (excludes.some(e => e.type === 'USER' && e.targetId === member.id)) { return true; } // 역할 ID 체크 if (excludes.some(e => e.type === 'ROLE' && member.roles.cache.has(e.targetId))) { return true; } return false; } /** * 서버 전체에 역할을 소급 적용합니다 (백그라운드 처리). */ async applyRetroactively(guild: Guild, roleId: string, initiatorId: string) { const role = guild.roles.cache.get(roleId); if (!role) 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.`); return; } // 모든 멤버 페치 (캐시되지 않았을 수 있음) const members = await guild.members.fetch(); const targets = members.filter(m => !m.user.bot && !m.roles.cache.has(roleId)); logger.info(`AutoRole: Starting retroactive application of role ${role.name} to ${targets.size} members in guild ${guild.id}`); let successCount = 0; let failCount = 0; // 비동기 실행 (응답 대기 안 함) (async () => { for (const [, member] of targets) { try { if (await this.isExcluded(member)) continue; await member.roles.add(role); successCount++; // Rate Limit 방지를 위한 지연 (1.5초) await new Promise(resolve => setTimeout(resolve, 1500)); } catch (error: any) { if (error.code === 10007 || error.code === 10013) { // Unknown Member / User (이미 나감) continue; } if (error.code === 50013) { // Missing Permissions (도중 권한 상실) logger.error(`AutoRole: Permission lost during retroactive assignment in guild ${guild.id}`); break; } failCount++; logger.error(`AutoRole: Failed to assign role to ${member.user.tag}:`, error); } } await auditLogService.log(guild, { 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}`, }); })(); } } export const autoRoleService = new AutoRoleService();