From 3762e554b40cb9c4b139d2a46f2d64edf8a7d078 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:11:40 -0500 Subject: [PATCH] Improved channelUpdate event logging --- src/commands/moderation/ban.ts | 5 +- src/events/channelEvents.ts | 116 +++++++++++++++++++++++++- src/util/logging/logAction.ts | 146 +++++++++++++++++++++++++++++++-- src/util/logging/types.ts | 14 +++- src/util/logging/utils.ts | 30 +++++++ 5 files changed, 295 insertions(+), 16 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index d834567..4cbc40f 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,7 +1,4 @@ -import { - PermissionsBitField, - SlashCommandBuilder, -} from 'discord.js'; +import { PermissionsBitField, SlashCommandBuilder } from 'discord.js'; import { updateMember, updateMemberModerationHistory } from '../../db/db.js'; import { parseDuration, scheduleUnban } from '../../util/helpers.js'; diff --git a/src/events/channelEvents.ts b/src/events/channelEvents.ts index 617f428..35e96ac 100644 --- a/src/events/channelEvents.ts +++ b/src/events/channelEvents.ts @@ -1,7 +1,98 @@ -import { AuditLogEvent, Events, GuildChannel } from 'discord.js'; +import { + AuditLogEvent, + Events, + GuildChannel, + PermissionOverwrites, +} from 'discord.js'; + import logAction from '../util/logging/logAction.js'; +import { ChannelLogAction } from '../util/logging/types.js'; import { Event } from '../types/EventTypes.js'; +function arePermissionsEqual( + oldPerms: Map, + newPerms: Map, +): boolean { + if (oldPerms.size !== newPerms.size) return false; + + for (const [id, oldPerm] of oldPerms.entries()) { + const newPerm = newPerms.get(id); + if (!newPerm) return false; + + if ( + !oldPerm.allow.equals(newPerm.allow) || + !oldPerm.deny.equals(newPerm.deny) + ) { + return false; + } + } + + return true; +} +function getPermissionChanges( + oldChannel: GuildChannel, + newChannel: GuildChannel, +): ChannelLogAction['permissionChanges'] { + const changes: ChannelLogAction['permissionChanges'] = []; + const newPerms = newChannel.permissionOverwrites.cache; + const oldPerms = oldChannel.permissionOverwrites.cache; + + for (const [id, newPerm] of newPerms.entries()) { + const oldPerm = oldPerms.get(id); + const targetType = newPerm.type === 0 ? 'role' : 'member'; + const targetName = + newPerm.type === 0 + ? newChannel.guild.roles.cache.get(id)?.name || id + : newChannel.guild.members.cache.get(id)?.user.username || id; + + if (!oldPerm) { + changes.push({ + action: 'added', + targetId: id, + targetType, + targetName, + allow: newPerm.allow, + deny: newPerm.deny, + }); + } else if ( + !oldPerm.allow.equals(newPerm.allow) || + !oldPerm.deny.equals(newPerm.deny) + ) { + changes.push({ + action: 'modified', + targetId: id, + targetType, + targetName, + oldAllow: oldPerm.allow, + oldDeny: oldPerm.deny, + newAllow: newPerm.allow, + newDeny: newPerm.deny, + }); + } + } + + for (const [id, oldPerm] of oldPerms.entries()) { + if (!newPerms.has(id)) { + const targetType = oldPerm.type === 0 ? 'role' : 'member'; + const targetName = + oldPerm.type === 0 + ? oldChannel.guild.roles.cache.get(id)?.name || id + : oldChannel.guild.members.cache.get(id)?.user.username || id; + + changes.push({ + action: 'removed', + targetId: id, + targetType, + targetName, + allow: oldPerm.allow, + deny: oldPerm.deny, + }); + } + } + + return changes; +} + export const channelCreate = { name: Events.ChannelCreate, execute: async (channel: GuildChannel) => { @@ -58,16 +149,33 @@ export const channelUpdate = { name: Events.ChannelUpdate, execute: async (oldChannel: GuildChannel, newChannel: GuildChannel) => { try { + if ( + oldChannel.name === newChannel.name && + oldChannel.type === newChannel.type && + oldChannel.permissionOverwrites.cache.size === + newChannel.permissionOverwrites.cache.size && + arePermissionsEqual( + oldChannel.permissionOverwrites.cache, + newChannel.permissionOverwrites.cache, + ) && + oldChannel.position !== newChannel.position + ) { + return; + } + const { guild } = newChannel; const auditLogs = await guild.fetchAuditLogs({ type: AuditLogEvent.ChannelUpdate, limit: 1, }); - const executor = auditLogs.entries.first()?.executor; + const log = auditLogs.entries.first(); + const executor = log?.executor; const moderator = executor ? await guild.members.fetch(executor.id) : undefined; + const permissionChanges = getPermissionChanges(oldChannel, newChannel); + await logAction({ guild, action: 'channelUpdate', @@ -75,8 +183,8 @@ export const channelUpdate = { moderator, oldName: oldChannel.name, newName: newChannel.name, - oldPermissions: oldChannel.permissionOverwrites.cache.first()?.allow, - newPermissions: newChannel.permissionOverwrites.cache.first()?.allow, + permissionChanges: + (permissionChanges ?? []).length > 0 ? permissionChanges : undefined, }); } catch (error) { console.error('Error handling channel update:', error); diff --git a/src/util/logging/logAction.ts b/src/util/logging/logAction.ts index 6c40810..049be63 100644 --- a/src/util/logging/logAction.ts +++ b/src/util/logging/logAction.ts @@ -19,6 +19,8 @@ import { createRoleChangeFields, getLogItemId, getEmojiForAction, + getPermissionDifference, + getPermissionNames, } from './utils.js'; export default async function logAction(payload: LogActionPayload) { @@ -209,6 +211,12 @@ export default async function logAction(payload: LogActionPayload) { } case 'channelUpdate': { + const changesExist = + payload.oldName !== payload.newName || + (payload.permissionChanges && payload.permissionChanges.length > 0); + + if (!changesExist) return; + fields.push({ name: '📝 Channel Information', value: [ @@ -223,12 +231,138 @@ export default async function logAction(payload: LogActionPayload) { inline: false, }); - if (payload.oldPermissions && payload.newPermissions) { - const permissionChanges = createPermissionChangeFields( - payload.oldPermissions, - payload.newPermissions, - ); - fields.push(...permissionChanges); + 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( diff --git a/src/util/logging/types.ts b/src/util/logging/types.ts index 414ffe6..36eed72 100644 --- a/src/util/logging/types.ts +++ b/src/util/logging/types.ts @@ -108,8 +108,18 @@ export interface ChannelLogAction extends BaseLogAction { channel: GuildChannel; oldName?: string; newName?: string; - oldPermissions?: Readonly; - newPermissions?: Readonly; + permissionChanges?: Array<{ + action: 'added' | 'modified' | 'removed'; + targetId: string; + targetType: 'role' | 'member'; + targetName: string; + allow?: Readonly; + deny?: Readonly; + oldAllow?: Readonly; + oldDeny?: Readonly; + newAllow?: Readonly; + newDeny?: Readonly; + }>; moderator?: GuildMember; } diff --git a/src/util/logging/utils.ts b/src/util/logging/utils.ts index f4044e4..d47d0ce 100644 --- a/src/util/logging/utils.ts +++ b/src/util/logging/utils.ts @@ -84,6 +84,36 @@ export const createPermissionChangeFields = ( return fields; }; +export const getPermissionNames = ( + permissions: Readonly, +): string[] => { + const names: string[] = []; + + Object.keys(PermissionsBitField.Flags).forEach((perm) => { + if (permissions.has(perm as keyof typeof PermissionsBitField.Flags)) { + names.push(formatPermissionName(perm)); + } + }); + + return names; +}; + +export const getPermissionDifference = ( + a: Readonly, + b: Readonly, +): string[] => { + const names: string[] = []; + + Object.keys(PermissionsBitField.Flags).forEach((perm) => { + const permKey = perm as keyof typeof PermissionsBitField.Flags; + if (a.has(permKey) && !b.has(permKey)) { + names.push(formatPermissionName(perm)); + } + }); + + return names; +}; + export const createRoleChangeFields = ( oldRole: Partial, newRole: Partial,