Improved channelUpdate event logging

This commit is contained in:
Ahmad 2025-03-01 00:11:40 -05:00
parent 6c523bbeba
commit 3762e554b4
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
5 changed files with 295 additions and 16 deletions

View file

@ -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';

View file

@ -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<string, PermissionOverwrites>,
newPerms: Map<string, PermissionOverwrites>,
): 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);

View file

@ -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(

View file

@ -108,8 +108,18 @@ export interface ChannelLogAction extends BaseLogAction {
channel: GuildChannel;
oldName?: string;
newName?: string;
oldPermissions?: Readonly<PermissionsBitField>;
newPermissions?: Readonly<PermissionsBitField>;
permissionChanges?: Array<{
action: 'added' | 'modified' | 'removed';
targetId: string;
targetType: 'role' | 'member';
targetName: string;
allow?: Readonly<PermissionsBitField>;
deny?: Readonly<PermissionsBitField>;
oldAllow?: Readonly<PermissionsBitField>;
oldDeny?: Readonly<PermissionsBitField>;
newAllow?: Readonly<PermissionsBitField>;
newDeny?: Readonly<PermissionsBitField>;
}>;
moderator?: GuildMember;
}

View file

@ -84,6 +84,36 @@ export const createPermissionChangeFields = (
return fields;
};
export const getPermissionNames = (
permissions: Readonly<PermissionsBitField>,
): 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<PermissionsBitField>,
b: Readonly<PermissionsBitField>,
): 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<RoleProperties>,
newRole: Partial<RoleProperties>,