poixpixel-discord-bot/src/util/logging/logAction.ts
2025-03-08 00:29:19 -05:00

417 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ButtonBuilder>().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: `<t:${Math.floor(payload.member.user.createdTimestamp / 1000)}:R>`,
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,
});
}