Kord/src/services/AutoRoleService.ts

164 lines
4.6 KiB
TypeScript

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<boolean> {
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();