import { TextChannel, ButtonStyle, ButtonBuilder, ActionRowBuilder, GuildChannel, } from 'discord.js'; import { LogActionPayload, ModerationLogAction, RoleUpdateAction, } from './types.js'; import { ACTION_COLORS, CHANNEL_TYPES } from './constants.js'; import { createUserField, createModeratorField, createChannelField, createPermissionChangeFields, createRoleChangeFields, getLogItemId, getEmojiForAction, getPermissionDifference, getPermissionNames, } from './utils.js'; import { loadConfig } from '../configLoader.js'; export default async function logAction(payload: LogActionPayload) { const config = loadConfig(); const logChannel = payload.guild.channels.cache.get(config.channels.logs); if (!logChannel?.isTextBased()) { console.error('Log channel not found or is not a Text Channel.'); return; } const fields = []; const components = []; switch (payload.action) { case 'ban': case 'kick': case 'mute': case 'unban': case 'unmute': case 'warn': { const moderationPayload = payload as ModerationLogAction; fields.push( createUserField(moderationPayload.target, 'User'), createModeratorField(moderationPayload.moderator, 'Moderator')!, { name: 'Reason', value: moderationPayload.reason, inline: false }, ); if (moderationPayload.duration) { fields.push({ name: 'Duration', value: moderationPayload.duration, inline: true, }); } break; } case 'messageDelete': { if (!payload.message.guild) return; fields.push( createUserField(payload.message.author, 'Author'), createChannelField(payload.message.channel as GuildChannel), { name: 'Content', value: payload.message.content || '*No content*', inline: false, }, ); break; } case 'messageEdit': { if (!payload.message.guild) return; fields.push( createUserField(payload.message.author, 'Author'), createChannelField(payload.message.channel as GuildChannel), { name: 'Before', value: payload.oldContent || '*No content*', inline: false, }, { name: 'After', value: payload.newContent || '*No content*', inline: false, }, ); components.push( new ActionRowBuilder().addComponents( new ButtonBuilder() .setLabel('Jump to Message') .setStyle(ButtonStyle.Link) .setURL(payload.message.url), ), ); break; } case 'memberJoin': case 'memberLeave': { fields.push(createUserField(payload.member, 'User'), { name: 'Account Created', value: ``, inline: true, }); break; } case 'memberUsernameUpdate': case 'memberNicknameUpdate': { const isUsername = payload.action === 'memberUsernameUpdate'; fields.push(createUserField(payload.member, 'User'), { name: '📝 Change Details', value: [ `**Type:** ${isUsername ? 'Username' : 'Nickname'} Update`, `**Before:** ${payload.oldValue}`, `**After:** ${payload.newValue}`, ].join('\n'), inline: false, }); break; } case 'roleAdd': case 'roleRemove': { fields.push(createUserField(payload.member, 'User'), { name: 'Role', value: payload.role.name, inline: true, }); const moderatorField = createModeratorField( payload.moderator, 'Added/Removed By', ); if (moderatorField) fields.push(moderatorField); break; } case 'roleCreate': case 'roleDelete': { fields.push( { name: 'Role Name', value: payload.role.name, inline: true }, { name: 'Role Color', value: payload.role.hexColor || 'No Color', inline: true, }, { name: 'Hoisted', value: payload.role.hoist ? 'Yes' : 'No', inline: true, }, { name: 'Mentionable', value: payload.role.mentionable ? 'Yes' : 'No', inline: true, }, ); const moderatorField = createModeratorField( payload.moderator, payload.action === 'roleCreate' ? 'Created By' : 'Deleted By', ); if (moderatorField) fields.push(moderatorField); break; } case 'roleUpdate': { const rolePayload = payload as RoleUpdateAction; fields.push({ name: '📝 Role Information', value: [ `**Name:** ${rolePayload.role.name}`, `**Color:** ${rolePayload.role.hexColor}`, `**Position:** ${rolePayload.role.position}`, ].join('\n'), inline: false, }); const changes = createRoleChangeFields( rolePayload.oldRole, rolePayload.newRole, ); if (changes.length) { fields.push({ name: '🔄 Changes Made', value: changes .map((field) => `**${field.name}:** ${field.value}`) .join('\n'), inline: false, }); } const permissionChanges = createPermissionChangeFields( rolePayload.oldPermissions, rolePayload.newPermissions, ); fields.push(...permissionChanges); const moderatorField = createModeratorField( rolePayload.moderator, '👤 Modified By', ); if (moderatorField) fields.push(moderatorField); break; } case 'channelUpdate': { const changesExist = payload.oldName !== payload.newName || (payload.permissionChanges && payload.permissionChanges.length > 0); if (!changesExist) return; fields.push({ name: '📝 Channel Information', value: [ `**Channel:** <#${payload.channel.id}>`, `**Type:** ${CHANNEL_TYPES[payload.channel.type]}`, payload.oldName !== payload.newName ? `**Name Change:** ${payload.oldName} → ${payload.newName}` : null, ] .filter(Boolean) .join('\n'), inline: false, }); if (payload.permissionChanges && payload.permissionChanges.length > 0) { const changes = { added: payload.permissionChanges.filter((c) => c.action === 'added'), modified: payload.permissionChanges.filter( (c) => c.action === 'modified', ), removed: payload.permissionChanges.filter( (c) => c.action === 'removed', ), }; if (changes.added.length > 0) { fields.push({ name: '➕ Added Permissions', value: changes.added .map((c) => { const targetMention = c.targetType === 'role' ? `<@&${c.targetId}>` : `<@${c.targetId}>`; return `For ${c.targetType} ${targetMention} (${c.targetName})`; }) .join('\n'), inline: false, }); changes.added.forEach((c) => { if (c.allow?.bitfield || c.deny?.bitfield) { const permList = []; if (c.allow?.bitfield) { const allowedPerms = getPermissionNames(c.allow); if (allowedPerms.length) { permList.push(`✅ **Allowed:** ${allowedPerms.join(', ')}`); } } if (c.deny?.bitfield) { const deniedPerms = getPermissionNames(c.deny); if (deniedPerms.length) { permList.push(`❌ **Denied:** ${deniedPerms.join(', ')}`); } } if (permList.length > 0) { fields.push({ name: `Permissions for ${c.targetType} ${c.targetName}`, value: permList.join('\n'), inline: false, }); } } }); } if (changes.modified.length > 0) { fields.push({ name: '🔄 Modified Permissions', value: changes.modified .map((c) => { const targetMention = c.targetType === 'role' ? `<@&${c.targetId}>` : `<@${c.targetId}>`; return `For ${c.targetType} ${targetMention} (${c.targetName})`; }) .join('\n'), inline: false, }); changes.modified.forEach((c) => { if (c.oldAllow && c.newAllow && c.oldDeny && c.newDeny) { const addedPerms = getPermissionDifference( c.newAllow, c.oldAllow, ); const removedPerms = getPermissionDifference( c.oldAllow, c.newAllow, ); const addedDenies = getPermissionDifference(c.newDeny, c.oldDeny); const removedDenies = getPermissionDifference( c.oldDeny, c.newDeny, ); const permissionChanges = []; if (addedPerms.length) { permissionChanges.push( `✅ **Newly Allowed:** ${addedPerms.join(', ')}`, ); } if (removedPerms.length) { permissionChanges.push( `⬇️ **No Longer Allowed:** ${removedPerms.join(', ')}`, ); } if (addedDenies.length) { permissionChanges.push( `❌ **Newly Denied:** ${addedDenies.join(', ')}`, ); } if (removedDenies.length) { permissionChanges.push( `⬆️ **No Longer Denied:** ${removedDenies.join(', ')}`, ); } if (permissionChanges.length > 0) { fields.push({ name: `Changes for ${c.targetType} ${c.targetName}`, value: permissionChanges.join('\n'), inline: false, }); } } }); } if (changes.removed.length > 0) { fields.push({ name: '➖ Removed Permissions', value: changes.removed .map((c) => { const targetMention = c.targetType === 'role' ? `<@&${c.targetId}>` : `<@${c.targetId}>`; return `For ${c.targetType} ${targetMention} (${c.targetName})`; }) .join('\n'), inline: false, }); } } const moderatorField = createModeratorField( payload.moderator, '👤 Modified By', ); if (moderatorField) fields.push(moderatorField); break; } case 'channelCreate': case 'channelDelete': { fields.push( { name: 'Channel', value: `<#${payload.channel.id}> (#${payload.channel.name})`, inline: true, }, { name: 'Type', value: CHANNEL_TYPES[payload.channel.type] || String(payload.channel.type), inline: true, }, ); const moderatorField = createModeratorField( payload.moderator, 'Created/Deleted By', ); if (moderatorField) fields.push(moderatorField); break; } } const logEmbed = { color: ACTION_COLORS[payload.action] || ACTION_COLORS.default, title: `${getEmojiForAction(payload.action)} ${payload.action.toUpperCase()}`, fields: fields.filter(Boolean), timestamp: new Date().toISOString(), footer: { text: `ID: ${getLogItemId(payload)}`, }, }; await logChannel.send({ embeds: [logEmbed], components: components.length ? components : undefined, }); }