164 lines
4.6 KiB
TypeScript
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();
|