refactor: remove invite tracking functionality and migrate member join handling to AutoRoleService
This commit is contained in:
parent
c4f5e8d53c
commit
6072ab716f
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `InviteRole` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "InviteRole";
|
||||||
|
|
@ -17,16 +17,6 @@ model GuildConfig {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model InviteRole {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
guildId String
|
|
||||||
inviteCode String
|
|
||||||
roleId String
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
|
|
||||||
@@unique([guildId, inviteCode])
|
|
||||||
}
|
|
||||||
|
|
||||||
model UserSubscription {
|
model UserSubscription {
|
||||||
userId String @id
|
userId String @id
|
||||||
tier SubscriptionTier @default(FREE)
|
tier SubscriptionTier @default(FREE)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ export class KordClient extends Client {
|
||||||
GatewayIntentBits.GuildVoiceStates,
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
GatewayIntentBits.GuildInvites,
|
|
||||||
GatewayIntentBits.GuildMembers,
|
GatewayIntentBits.GuildMembers,
|
||||||
],
|
],
|
||||||
partials: [Partials.Message, Partials.Channel, Partials.GuildMember],
|
partials: [Partials.Message, Partials.Channel, Partials.GuildMember],
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { Events, Guild } from 'discord.js';
|
import { Events, Guild } from 'discord.js';
|
||||||
import { InviteService } from '../services/InviteService';
|
|
||||||
import { PresenceService } from '../services/PresenceService';
|
import { PresenceService } from '../services/PresenceService';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.GuildCreate,
|
name: Events.GuildCreate,
|
||||||
once: false,
|
once: false,
|
||||||
async execute(guild: Guild) {
|
async execute(guild: Guild) {
|
||||||
await InviteService.cacheGuildInvites(guild);
|
|
||||||
PresenceService.updatePresence(guild.client);
|
PresenceService.updatePresence(guild.client);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Events, GuildMember } from 'discord.js';
|
import { Events, GuildMember } from 'discord.js';
|
||||||
import { InviteService } from '../services/InviteService';
|
import { autoRoleService } from '../services/AutoRoleService';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.GuildMemberAdd,
|
name: Events.GuildMemberAdd,
|
||||||
once: false,
|
once: false,
|
||||||
async execute(member: GuildMember) {
|
async execute(member: GuildMember) {
|
||||||
await InviteService.handleMemberAdd(member);
|
await autoRoleService.handleMemberJoin(member);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Events } from 'discord.js';
|
import { Events } from 'discord.js';
|
||||||
import { KordClient } from '../client/KordClient';
|
import { KordClient } from '../client/KordClient';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { InviteService } from '../services/InviteService';
|
|
||||||
import { VoiceService } from '../services/VoiceService';
|
import { VoiceService } from '../services/VoiceService';
|
||||||
import { PresenceService } from '../services/PresenceService';
|
import { PresenceService } from '../services/PresenceService';
|
||||||
import { EventService } from '../services/EventService';
|
import { EventService } from '../services/EventService';
|
||||||
|
|
@ -14,7 +13,6 @@ export default {
|
||||||
once: true,
|
once: true,
|
||||||
async execute(client: KordClient) {
|
async execute(client: KordClient) {
|
||||||
logger.info(`Ready! Logged in as ${client.user?.tag}`);
|
logger.info(`Ready! Logged in as ${client.user?.tag}`);
|
||||||
await InviteService.cacheAllInvites(client);
|
|
||||||
await VoiceService.syncChannels(client);
|
await VoiceService.syncChannels(client);
|
||||||
PresenceService.startActivePresence(client);
|
PresenceService.startActivePresence(client);
|
||||||
EventService.startReminderLoop(client);
|
EventService.startReminderLoop(client);
|
||||||
|
|
|
||||||
|
|
@ -211,26 +211,6 @@ export const en: TranslationSchema = {
|
||||||
permissionsError: 'Failed to assign role due to low bot hierarchy or missing permissions.',
|
permissionsError: 'Failed to assign role due to low bot hierarchy or missing permissions.',
|
||||||
suspendNotice: 'Auto role assignment has been suspended due to insufficient permissions. Please check the bot\'s permissions and role hierarchy.',
|
suspendNotice: 'Auto role assignment has been suspended due to insufficient permissions. Please check the bot\'s permissions and role hierarchy.',
|
||||||
},
|
},
|
||||||
invite: {
|
|
||||||
description: 'Manage roles mapped to invite codes.',
|
|
||||||
listDescription: 'List all invite codes in the server.',
|
|
||||||
linkDescription: 'Link a role to an existing invite code.',
|
|
||||||
createDescription: 'Create a new invite code with a mapped role.',
|
|
||||||
unlinkDescription: 'Unlink a role from an invite code.',
|
|
||||||
codeOption: 'Invite code string',
|
|
||||||
roleOption: 'Role to assign',
|
|
||||||
usesOption: 'Maximum uses',
|
|
||||||
ageOption: 'Expiration (seconds, 0=unlimited)',
|
|
||||||
filterOption: 'Lookup filter (all=all, managed=managed only)',
|
|
||||||
listTitle: 'Invite Code Mappings',
|
|
||||||
listEmpty: 'No invite codes are currently linked to roles.',
|
|
||||||
linkSuccess: 'Role {{role}} has been linked to invite code `{{code}}`.',
|
|
||||||
unlinkSuccess: 'Role link has been removed from invite code `{{code}}`.',
|
|
||||||
createSuccess: 'New invite code `{{code}}` has been created and linked to role {{role}}.',
|
|
||||||
expireWarning: 'Invite code `{{code}}` has expired or been deleted. Role link has been removed.',
|
|
||||||
identifyFail: 'Could not identify the invite code for the joining user.',
|
|
||||||
identifyFailDesc: 'Due to simultaneous joins, the invite code for {{user}} could not be determined. Only default roles were assigned.',
|
|
||||||
},
|
|
||||||
music: {
|
music: {
|
||||||
description: 'Play YouTube audio in voice channels.',
|
description: 'Play YouTube audio in voice channels.',
|
||||||
addDescription: 'Search YouTube or add a video URL to the queue.',
|
addDescription: 'Search YouTube or add a video URL to the queue.',
|
||||||
|
|
@ -348,8 +328,6 @@ export const en: TranslationSchema = {
|
||||||
VOICE_GLOBAL: 'Voice Channels (Global)',
|
VOICE_GLOBAL: 'Voice Channels (Global)',
|
||||||
VOICE_GENERATOR_CHANNEL: 'Voice Generator Channel',
|
VOICE_GENERATOR_CHANNEL: 'Voice Generator Channel',
|
||||||
VOICE_GENERATOR_CATEGORY: 'Voice Generator Category',
|
VOICE_GENERATOR_CATEGORY: 'Voice Generator Category',
|
||||||
INVITE_TRACKING: 'Invite Tracking',
|
|
||||||
INVITE_ROLE_HIERARCHY: 'Invite Role Assignment (Hierarchy)',
|
|
||||||
MIMIC_WEBHOOK: 'Message Mimic (Webhook)',
|
MIMIC_WEBHOOK: 'Message Mimic (Webhook)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -408,7 +386,6 @@ export const en: TranslationSchema = {
|
||||||
BOOT: 'Boot',
|
BOOT: 'Boot',
|
||||||
VOICE: 'Voice',
|
VOICE: 'Voice',
|
||||||
PERMISSION: 'Permission',
|
PERMISSION: 'Permission',
|
||||||
INVITE: 'Invite',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -211,26 +211,6 @@ export const ko: TranslationSchema = {
|
||||||
permissionsError: '봇의 역할 순위가 낮거나 권한이 부족하여 역할을 부여할 수 없습니다.',
|
permissionsError: '봇의 역할 순위가 낮거나 권한이 부족하여 역할을 부여할 수 없습니다.',
|
||||||
suspendNotice: '권한 부족으로 인해 자동 역할 부여 기능이 일시 중지되었습니다. 봇의 권한과 역할 순위를 확인해 주세요.',
|
suspendNotice: '권한 부족으로 인해 자동 역할 부여 기능이 일시 중지되었습니다. 봇의 권한과 역할 순위를 확인해 주세요.',
|
||||||
},
|
},
|
||||||
invite: {
|
|
||||||
description: '초대 코드와 역할을 연동하여 관리합니다.',
|
|
||||||
listDescription: '서버의 초대 코드 목록을 조회합니다.',
|
|
||||||
linkDescription: '기존 초대 코드에 역할을 연동합니다.',
|
|
||||||
createDescription: '역할이 연동된 새로운 초대 코드를 생성합니다.',
|
|
||||||
unlinkDescription: '초대 코드에 연동된 역할을 해제합니다.',
|
|
||||||
codeOption: '초대 코드 문자열',
|
|
||||||
roleOption: '부여할 역할',
|
|
||||||
usesOption: '최대 사용 횟수',
|
|
||||||
ageOption: '만료 기간(초, 0=무제한)',
|
|
||||||
filterOption: '조회 필터 (all=전체, managed=관리 중)',
|
|
||||||
listTitle: '초대 코드 매핑 목록',
|
|
||||||
listEmpty: '연동된 초대 코드가 없습니다.',
|
|
||||||
linkSuccess: '초대 코드 `{{code}}`에 {{role}} 역할이 연동되었습니다.',
|
|
||||||
unlinkSuccess: '초대 코드 `{{code}}`의 역할 연동이 해제되었습니다.',
|
|
||||||
createSuccess: '새로운 초대 코드 `{{code}}`가 생성되었으며 {{role}} 역할이 연동되었습니다.',
|
|
||||||
expireWarning: '초대 코드 `{{code}}`가 만료/삭제되어 역할 연동이 해제되었습니다.',
|
|
||||||
identifyFail: '참여한 유저의 초대 코드를 식별하지 못했습니다.',
|
|
||||||
identifyFailDesc: '동시 접속 등의 이유로 {{user}}님의 초대 코드를 확정할 수 없어 기본 역할만 부여되었습니다.',
|
|
||||||
},
|
|
||||||
music: {
|
music: {
|
||||||
description: 'Play YouTube audio in voice channels.',
|
description: 'Play YouTube audio in voice channels.',
|
||||||
addDescription: 'Search YouTube or add a video URL to the queue.',
|
addDescription: 'Search YouTube or add a video URL to the queue.',
|
||||||
|
|
@ -348,8 +328,6 @@ export const ko: TranslationSchema = {
|
||||||
VOICE_GLOBAL: '임시 음성 채널 (전역)',
|
VOICE_GLOBAL: '임시 음성 채널 (전역)',
|
||||||
VOICE_GENERATOR_CHANNEL: '음성 생성기 채널',
|
VOICE_GENERATOR_CHANNEL: '음성 생성기 채널',
|
||||||
VOICE_GENERATOR_CATEGORY: '음성 생성기 카테고리',
|
VOICE_GENERATOR_CATEGORY: '음성 생성기 카테고리',
|
||||||
INVITE_TRACKING: '초대 추적',
|
|
||||||
INVITE_ROLE_HIERARCHY: '초대 역할 부여 (계층 검사)',
|
|
||||||
MIMIC_WEBHOOK: '메시지 흉내 (Webhook)',
|
MIMIC_WEBHOOK: '메시지 흉내 (Webhook)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -408,7 +386,6 @@ export const ko: TranslationSchema = {
|
||||||
BOOT: '부팅',
|
BOOT: '부팅',
|
||||||
VOICE: '음성',
|
VOICE: '음성',
|
||||||
PERMISSION: '권한',
|
PERMISSION: '권한',
|
||||||
INVITE: '초대',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { prisma } from '../database';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
|
|
||||||
export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR';
|
export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR';
|
||||||
export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC';
|
export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'MIMIC';
|
||||||
|
|
||||||
export interface AuditLogPayload {
|
export interface AuditLogPayload {
|
||||||
category: AuditCategory;
|
category: AuditCategory;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,35 @@ export class AutoRoleService {
|
||||||
async setEnabled(guildId: string, enabled: boolean) {
|
async setEnabled(guildId: string, enabled: boolean) {
|
||||||
return this.updateConfig(guildId, { isEnabled: enabled });
|
return this.updateConfig(guildId, { isEnabled: enabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 신규 멤버가 입장했을 때 자동으로 역할을 부여합니다.
|
||||||
|
*/
|
||||||
|
async handleMemberJoin(member: GuildMember) {
|
||||||
|
const config = await this.getConfig(member.guild.id);
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const isBot = member.user.bot;
|
||||||
|
const isEnabled = isBot ? config.botEnabled : config.isEnabled;
|
||||||
|
const roleIds = isBot ? config.botRoleIds : config.userRoleIds;
|
||||||
|
|
||||||
|
if (!isEnabled || roleIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await member.roles.add(roleIds, 'Kord Auto-Role');
|
||||||
|
logger.info(`[AutoRole] Added roles to ${member.user.tag} in ${member.guild.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AutoRole] Failed to add roles to ${member.user.tag} in ${member.guild.name}`, error);
|
||||||
|
|
||||||
|
// 권한 문제인 경우 감사 로그에 기록
|
||||||
|
await auditLogService.log(member.guild, {
|
||||||
|
category: 'PERMISSION',
|
||||||
|
severity: 'WARN',
|
||||||
|
title: 'Auto-Role Failure',
|
||||||
|
description: `Failed to assign roles to ${member.user.toString()} automatically. Please check the bot's permission and role hierarchy.`
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const autoRoleService = new AutoRoleService();
|
export const autoRoleService = new AutoRoleService();
|
||||||
|
|
|
||||||
|
|
@ -109,24 +109,7 @@ const FEATURE_DEFINITIONS: FeatureDefinition[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── 5. 초대 추적 ──
|
// ── 5. 메시지 흉내 (Mimic) ──
|
||||||
{
|
|
||||||
featureKey: 'INVITE_TRACKING',
|
|
||||||
scope: 'guild',
|
|
||||||
permissions: [PermissionFlagsBits.ManageGuild],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── 6. 역할 자동 부여 (초대 연동) - 계층 검사 ──
|
|
||||||
{
|
|
||||||
featureKey: 'INVITE_ROLE_HIERARCHY',
|
|
||||||
scope: 'hierarchy',
|
|
||||||
resolveTargetRoleIds: async (guildId) => {
|
|
||||||
const inviteRoles = await prisma.inviteRole.findMany({ where: { guildId } });
|
|
||||||
return inviteRoles.map((ir: { roleId: string }) => ir.roleId);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── 7. 메시지 흉내 (Mimic) ──
|
|
||||||
{
|
{
|
||||||
featureKey: 'MIMIC_WEBHOOK',
|
featureKey: 'MIMIC_WEBHOOK',
|
||||||
scope: 'guild',
|
scope: 'guild',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue