import { AuditLogChange, AuditLogEvent, AuditLogOptionsType, GuildAuditLogsEntry, APIRole, time, TimestampStyles, PermissionsBitField, PermissionsString, Guild } from 'discord.js'; import { PermissionsChangeLog, PinExtras, RoleChangeLog, TimeoutChangeLog } from '../types'; import { getArrayDifference } from './array-utils'; export const enum TargetType { Unknown = 0, Role = 1, User = 2, Channel = 3 }; const TARGETS_ROLES: AuditLogEvent[] = [ AuditLogEvent.RoleCreate, AuditLogEvent.RoleDelete, AuditLogEvent.RoleUpdate ]; const TARGETS_USERS: AuditLogEvent[] = [ AuditLogEvent.MemberUpdate, AuditLogEvent.MemberKick, AuditLogEvent.MemberBanAdd, AuditLogEvent.MemberBanRemove, AuditLogEvent.MemberRoleUpdate, AuditLogEvent.MessageDelete, AuditLogEvent.MessagePin, AuditLogEvent.MessageUnpin ]; const TARGETS_CHANNELS: AuditLogEvent[] = [ AuditLogEvent.ChannelCreate, AuditLogEvent.ChannelUpdate, AuditLogEvent.ChannelDelete, AuditLogEvent.ThreadCreate, AuditLogEvent.ThreadUpdate, AuditLogEvent.ThreadDelete, AuditLogEvent.ChannelOverwriteCreate, AuditLogEvent.ChannelOverwriteDelete, AuditLogEvent.ChannelOverwriteUpdate ]; export function getTargetType(actionType: AuditLogEvent): TargetType { if (TARGETS_ROLES.includes(actionType)) return TargetType.Role; else if (TARGETS_USERS.includes(actionType)) return TargetType.User; else if (TARGETS_CHANNELS.includes(actionType)) return TargetType.Channel; return TargetType.Unknown; } export function formatSnowflake(snowflake: string, targetType: TargetType): string { if (targetType == TargetType.Role) return `<@&${snowflake}>`; else if (targetType == TargetType.User) return `<@${snowflake}>`; else if (targetType == TargetType.Channel) return `<#${snowflake}>`; return snowflake; } export function formatChanges(entry: GuildAuditLogsEntry): string { return entry.changes.map(c => formatChange(c)).filter(e => e).join('\n'); } export function formatExtras(entry: GuildAuditLogsEntry, guild: Guild): string { if (entry.action == AuditLogEvent.MessagePin || entry.action == AuditLogEvent.MessageUnpin) return formatMessagePin(entry.extra as unknown as PinExtras, guild); if (entry.action == AuditLogEvent.ChannelOverwriteCreate || entry.action == AuditLogEvent.ChannelOverwriteDelete || entry.action == AuditLogEvent.ChannelOverwriteUpdate) { return `Target: ${entry.extra!.toString()}`; } try { const reserialized = JSON.parse(JSON.stringify(entry)); const results: string[] = []; for (const [key, value] of Object.entries(reserialized.extra ?? {})) { if (!value) continue; if (key == 'channel_id') results.push(formatSnowflake(value as string, TargetType.Channel)); results.push(`${key}: ${value}`); } return results.join('\n'); } catch (e) { console.error(e); return ''; } return ''; } function formatChange(change: AuditLogChange): string | undefined { switch (change.key) { case '$add': case '$remove': return formatMemberRoleChange(change); case 'communication_disabled_until': return formatTimeoutChange(change); case 'permissions': case 'allow': case 'deny': return formatPermissionOrOverwrites(change as PermissionsChangeLog); } if (change.new !== undefined && change.old === undefined) return `Set ${change.key} to ${change.new}`; if (change.new === undefined && change.old !== undefined) return `Set ${change.key} with value ${change.old} to default/null`; return `Set ${change.key} from ${change.old} to ${change.new}`; } function formatMemberRoleChange(change: RoleChangeLog): string | undefined { if (!change.new) return; const changes: string[] = []; for (const roleChange of change.new) { if (change.key == '$add') changes.push(`Added role ${formatSnowflake(roleChange.id, TargetType.Role)}`); else changes.push(`Removed role ${formatSnowflake(roleChange.id, TargetType.Role)}`); } if (changes.length == 0) return; return changes.join('\n'); } function formatTimeoutChange(change: TimeoutChangeLog): string { if (!change.new) return 'Timeout removed'; const date = new Date(change.new); return `Timeout until ${time(date, TimestampStyles.RelativeTime)}`; } function formatPermissionOrOverwrites(change: PermissionsChangeLog): string { const oldPerms = new PermissionsBitField(BigInt(change.old ?? 0)).toArray(); const newPerms = new PermissionsBitField(BigInt(change.new ?? 0)).toArray(); switch (change.key) { case 'permissions': return formatPermissionChange(oldPerms, newPerms, 'Removed permission(s)', 'Added permission(s)'); case 'allow': return formatPermissionChange(oldPerms, newPerms, 'Allow removed', 'Allow added'); case 'deny': return formatPermissionChange(oldPerms, newPerms, 'Deny removed', 'Deny added'); default: return formatPermissionChange(oldPerms, newPerms, `${change.key} removed`, `${change.key} added`); } } function formatPermissionChange(oldPermissions: PermissionsString[], newPermissions: PermissionsString[], removedDescription: string, addedDescription: string) { const { added, removed } = getArrayDifference(oldPermissions, newPermissions); const result: string[] = []; if (added.length > 0) { result.push(`${addedDescription}: ${added.join(', ')}`); } if (removed.length > 0) { result.push(`${removedDescription}: ${removed.join(', ')}`); } return result.join('\n'); } function formatMessagePin(data: PinExtras, guild: Guild): string { return `Message: [${data.messageId}](https://discord.com/${guild.id}/${data.channel.id}/${data.messageId})`; }