Added Warn and Ban Commands, Added Logging, and Much More

This commit is contained in:
Ahmad 2025-02-23 21:39:49 -05:00
parent d89de72e08
commit 86adac3f08
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
33 changed files with 2200 additions and 204 deletions

23
src/util/configLoader.ts Normal file
View file

@ -0,0 +1,23 @@
import { Config } from '../types/ConfigTypes.js';
import fs from 'node:fs';
import path from 'node:path';
export function loadConfig(): Config {
try {
const configPath = path.join(process.cwd(), './config.json');
const configFile = fs.readFileSync(configPath, 'utf8');
const config: Config = JSON.parse(configFile);
const requiredFields = ['token', 'clientId', 'guildId'];
for (const field of requiredFields) {
if (!config[field as keyof Config]) {
throw new Error(`Missing required config field: ${field}`);
}
}
return config;
} catch (error) {
console.error('Failed to load config:', error);
process.exit(1);
}
}

View file

@ -1,52 +0,0 @@
import fs from 'node:fs';
import pkg from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { memberTable } from '../db/schema.js';
import { eq } from 'drizzle-orm';
const { Pool } = pkg;
const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
const { dbConnectionString } = config;
const dbPool = new Pool({
connectionString: dbConnectionString,
ssl: true,
});
const db = drizzle({ client: dbPool });
export async function getAllMembers() {
return await db.select().from(memberTable);
}
export async function setMembers(nonBotMembers: any) {
nonBotMembers.forEach(async (member: any) => {
const memberExists = await db
.select()
.from(memberTable)
.where(eq(memberTable.discordId, member.user.id));
if (memberExists.length > 0) {
await db
.update(memberTable)
.set({ discordUsername: member.user.username })
.where(eq(memberTable.discordId, member.user.id));
}
else {
const members: typeof memberTable.$inferInsert = {
discordId: member.user.id,
discordUsername: member.user.username,
};
await db.insert(memberTable).values(members);
}
});
}
export async function removeMember(discordId: string) {
await db.delete(memberTable).where(eq(memberTable.discordId, discordId));
}
export async function getMember(discordId: string) {
return await db
.select()
.from(memberTable)
.where(eq(memberTable.discordId, discordId));
}

View file

@ -1,9 +1,16 @@
import { REST, Routes } from 'discord.js';
import fs from 'fs';
import path from 'path';
import { loadConfig } from './configLoader.js';
const config = loadConfig();
const { token, clientId, guildId } = config;
const __dirname = path.resolve();
const commandsPath = path.join(__dirname, 'target', 'commands');
const rest = new REST({ version: '10' }).setToken(token);
const getFilesRecursively = (directory: string): string[] => {
const files: string[] = [];
const filesInDirectory = fs.readdirSync(directory);
@ -13,8 +20,7 @@ const getFilesRecursively = (directory: string): string[] => {
if (fs.statSync(filePath).isDirectory()) {
files.push(...getFilesRecursively(filePath));
}
else if (file.endsWith('.js')) {
} else if (file.endsWith('.js')) {
files.push(filePath);
}
}
@ -27,9 +33,13 @@ const commandFiles = getFilesRecursively(commandsPath);
export const deployCommands = async () => {
try {
console.log(
`Started refreshing ${commandFiles.length} application (/) commands.`,
`Started refreshing ${commandFiles.length} application (/) commands...`,
);
const existingCommands = (await rest.get(
Routes.applicationGuildCommands(clientId, guildId),
)) as any[];
const commands = commandFiles.map(async (file) => {
const commandModule = await import(`file://${file}`);
const command = commandModule.default;
@ -40,8 +50,7 @@ export const deployCommands = async () => {
'execute' in command
) {
return command;
}
else {
} else {
console.warn(
`[WARNING] The command at ${file} is missing a required "data" or "execute" property.`,
);
@ -53,9 +62,31 @@ export const deployCommands = async () => {
commands.filter((command) => command !== null),
);
const apiCommands = validCommands.map((command) => command.data.toJSON());
const commandsToRemove = existingCommands.filter(
(existingCmd) =>
!apiCommands.some((newCmd) => newCmd.name === existingCmd.name),
);
for (const cmdToRemove of commandsToRemove) {
await rest.delete(
Routes.applicationGuildCommand(clientId, guildId, cmdToRemove.id),
);
console.log(`Removed command: ${cmdToRemove.name}`);
}
const data: any = await rest.put(
Routes.applicationGuildCommands(clientId, guildId),
{ body: apiCommands },
);
console.log(
`Successfully registered ${data.length} application (/) commands with the Discord API.`,
);
return validCommands;
}
catch (error) {
} catch (error) {
console.error(error);
}
};

45
src/util/eventLoader.ts Normal file
View file

@ -0,0 +1,45 @@
import { Client } from 'discord.js';
import { readdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export async function registerEvents(client: Client): Promise<void> {
try {
const eventsPath = join(__dirname, '..', 'events');
const eventFiles = readdirSync(eventsPath).filter(
(file) => file.endsWith('.js') || file.endsWith('.ts'),
);
for (const file of eventFiles) {
const filePath = join(eventsPath, file);
const eventModule = await import(`file://${filePath}`);
const events =
eventModule.default || eventModule[`${file.split('.')[0]}Events`];
const eventArray = Array.isArray(events) ? events : [events];
for (const event of eventArray) {
if (!event?.name) {
console.warn(`Event in ${filePath} is missing a name property`);
continue;
}
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
console.log(`Registered event: ${event.name}`);
}
}
} catch (error) {
console.error('Error registering events:', error);
throw error;
}
}

165
src/util/helpers.ts Normal file
View file

@ -0,0 +1,165 @@
import Canvas from '@napi-rs/canvas';
import path from 'path';
import { AttachmentBuilder, Client, GuildMember, Guild } from 'discord.js';
import { and, eq } from 'drizzle-orm';
import { moderationTable } from '../db/schema.js';
import { db, updateMember } from '../db/db.js';
import logAction from './logging/logAction.js';
const __dirname = path.resolve();
export function parseDuration(duration: string): number {
const regex = /^(\d+)(s|m|h|d)$/;
const match = duration.match(regex);
if (!match) throw new Error('Invalid duration format');
const value = parseInt(match[1]);
const unit = match[2];
switch (unit) {
case 's':
return value * 1000;
case 'm':
return value * 60 * 1000;
case 'h':
return value * 60 * 60 * 1000;
case 'd':
return value * 24 * 60 * 60 * 1000;
default:
throw new Error('Invalid duration unit');
}
}
interface generateMemberBannerTypes {
member: GuildMember;
width: number;
height: number;
}
export async function generateMemberBanner({
member,
width,
height,
}: generateMemberBannerTypes) {
const welcomeBackground = path.join(__dirname, 'assets', 'welcome-bg.png');
const canvas = Canvas.createCanvas(width, height);
const context = canvas.getContext('2d');
const background = await Canvas.loadImage(welcomeBackground);
const memberCount = member.guild.memberCount;
const avatarSize = 150;
const avatarY = height - avatarSize - 25;
const avatarX = width / 2 - avatarSize / 2;
context.drawImage(background, 0, 0, width, height);
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
context.fillRect(0, 0, width, height);
context.font = '60px Sans';
context.fillStyle = '#ffffff';
context.textAlign = 'center';
context.fillText('Welcome', width / 2, height / 3.25);
context.font = '40px Sans';
context.fillText(member.user.username, width / 2, height / 2.25);
context.font = '30px Sans';
context.fillText(`You are member #${memberCount}`, width / 2, height / 1.75);
context.beginPath();
context.arc(
width / 2,
height - avatarSize / 2 - 25,
avatarSize / 2,
0,
Math.PI * 2,
true,
);
context.closePath();
context.clip();
const avatarURL = member.user.displayAvatarURL({
extension: 'png',
size: 256,
});
const avatar = await Canvas.loadImage(avatarURL);
context.drawImage(avatar, avatarX, avatarY, avatarSize, avatarSize);
const attachment = new AttachmentBuilder(await canvas.encode('png'), {
name: 'welcome-image.png',
});
return attachment;
}
export async function scheduleUnban(
client: Client,
guildId: string,
userId: string,
expiresAt: Date,
) {
const timeUntilUnban = expiresAt.getTime() - Date.now();
if (timeUntilUnban > 0) {
setTimeout(async () => {
await executeUnban(client, guildId, userId);
}, timeUntilUnban);
}
}
export async function executeUnban(
client: Client,
guildId: string,
userId: string,
reason?: string,
) {
try {
const guild = await client.guilds.fetch(guildId);
await guild.members.unban(userId, reason ?? 'Temporary ban expired');
await db
.update(moderationTable)
.set({ active: false })
.where(
and(
eq(moderationTable.discordId, userId),
eq(moderationTable.action, 'ban'),
eq(moderationTable.active, true),
),
);
await updateMember({
discordId: userId,
currentlyBanned: false,
});
await logAction({
guild,
action: 'unban',
target: guild.members.cache.get(userId)!,
moderator: guild.members.cache.get(client.user!.id)!,
reason: reason ?? 'Temporary ban expired',
});
} catch (error) {
console.error(`Failed to unban user ${userId}:`, error);
}
}
export async function loadActiveBans(client: Client, guild: Guild) {
const activeBans = await db
.select()
.from(moderationTable)
.where(
and(eq(moderationTable.action, 'ban'), eq(moderationTable.active, true)),
);
for (const ban of activeBans) {
if (!ban.expiresAt) continue;
const timeUntilUnban = ban.expiresAt.getTime() - Date.now();
if (timeUntilUnban <= 0) {
await executeUnban(client, guild.id, ban.discordId);
} else {
await scheduleUnban(client, guild.id, ban.discordId, ban.expiresAt);
}
}
}

View file

@ -0,0 +1,65 @@
import { ChannelType } from 'discord.js';
import { LogActionType } from './types';
export const ACTION_COLORS: Record<string, number> = {
// Danger actions - Red
ban: 0xff0000,
kick: 0xff0000,
messageDelete: 0xff0000,
channelDelete: 0xff0000,
memberLeave: 0xff0000,
roleDelete: 0xff0000,
// Warning actions - Orange
warn: 0xffaa00,
mute: 0xffaa00,
roleUpdate: 0xffaa00,
memberUsernameUpdate: 0xffaa00,
memberNicknameUpdate: 0xffaa00,
channelUpdate: 0xffaa00,
messageUpdate: 0xffaa00,
// Success actions - Green
unban: 0x00ff00,
unmute: 0x00ff00,
memberJoin: 0x00aa00,
channelCreate: 0x00aa00,
roleAdd: 0x00aa00,
roleCreate: 0x00aa00,
// Default - Blue
default: 0x0099ff,
};
export const ACTION_EMOJIS: Record<LogActionType, string> = {
roleCreate: '⭐',
roleDelete: '🗑️',
roleUpdate: '📝',
channelCreate: '📢',
channelDelete: '🗑️',
channelUpdate: '🔧',
ban: '🔨',
kick: '👢',
mute: '🔇',
unban: '🔓',
unmute: '🔊',
warn: '⚠️',
messageDelete: '📝',
messageEdit: '✏️',
memberJoin: '👋',
memberLeave: '👋',
memberUsernameUpdate: '📝',
memberNicknameUpdate: '📝',
roleAdd: '',
roleRemove: '',
};
export const CHANNEL_TYPES: Record<number, string> = {
[ChannelType.GuildText]: 'Text Channel',
[ChannelType.GuildVoice]: 'Voice Channel',
[ChannelType.GuildCategory]: 'Category',
[ChannelType.GuildStageVoice]: 'Stage Channel',
[ChannelType.GuildForum]: 'Forum Channel',
[ChannelType.GuildAnnouncement]: 'Announcement Channel',
[ChannelType.GuildMedia]: 'Media Channel',
};

View file

@ -0,0 +1,276 @@
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,
} from './utils.js';
export default async function logAction(payload: LogActionPayload) {
const logChannel = payload.guild.channels.cache.get('1007787977432383611');
if (!logChannel || !(logChannel instanceof TextChannel)) {
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': {
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.oldPermissions && payload.newPermissions) {
const permissionChanges = createPermissionChangeFields(
payload.oldPermissions,
payload.newPermissions,
);
fields.push(...permissionChanges);
}
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}>`, 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,
});
}

124
src/util/logging/types.ts Normal file
View file

@ -0,0 +1,124 @@
import {
Guild,
GuildMember,
Message,
Role,
GuildChannel,
PermissionsBitField,
} from 'discord.js';
export type ModerationActionType =
| 'ban'
| 'kick'
| 'mute'
| 'unban'
| 'unmute'
| 'warn';
export type MessageActionType = 'messageDelete' | 'messageEdit';
export type MemberActionType =
| 'memberJoin'
| 'memberLeave'
| 'memberUsernameUpdate'
| 'memberNicknameUpdate';
export type RoleActionType =
| 'roleAdd'
| 'roleRemove'
| 'roleCreate'
| 'roleDelete'
| 'roleUpdate';
export type ChannelActionType =
| 'channelCreate'
| 'channelDelete'
| 'channelUpdate';
export type LogActionType =
| ModerationActionType
| MessageActionType
| MemberActionType
| RoleActionType
| ChannelActionType;
export type RoleProperties = {
name: string;
color: string;
hoist: boolean;
mentionable: boolean;
};
export interface BaseLogAction {
guild: Guild;
action: LogActionType;
moderator?: GuildMember;
reason?: string;
duration?: string;
}
export interface ModerationLogAction extends BaseLogAction {
action: ModerationActionType;
target: GuildMember;
moderator: GuildMember;
reason: string;
duration?: string;
}
export interface MessageLogAction extends BaseLogAction {
action: MessageActionType;
message: Message<true>;
oldContent?: string;
newContent?: string;
}
export interface MemberLogAction extends BaseLogAction {
action: 'memberJoin' | 'memberLeave';
member: GuildMember;
}
export interface MemberUpdateAction extends BaseLogAction {
action: 'memberUsernameUpdate' | 'memberNicknameUpdate';
member: GuildMember;
oldValue: string;
newValue: string;
}
export interface RoleLogAction extends BaseLogAction {
action: 'roleAdd' | 'roleRemove';
member: GuildMember;
role: Role;
moderator?: GuildMember;
}
export interface RoleUpdateAction extends BaseLogAction {
action: 'roleUpdate';
role: Role;
oldRole: Partial<RoleProperties>;
newRole: Partial<RoleProperties>;
oldPermissions: Readonly<PermissionsBitField>;
newPermissions: Readonly<PermissionsBitField>;
moderator?: GuildMember;
}
export interface RoleCreateDeleteAction extends BaseLogAction {
action: 'roleCreate' | 'roleDelete';
role: Role;
moderator?: GuildMember;
}
export interface ChannelLogAction extends BaseLogAction {
action: ChannelActionType;
channel: GuildChannel;
oldName?: string;
newName?: string;
oldPermissions?: Readonly<PermissionsBitField>;
newPermissions?: Readonly<PermissionsBitField>;
moderator?: GuildMember;
}
export type LogActionPayload =
| ModerationLogAction
| MessageLogAction
| MemberLogAction
| MemberUpdateAction
| RoleLogAction
| RoleCreateDeleteAction
| RoleUpdateAction
| ChannelLogAction;

163
src/util/logging/utils.ts Normal file
View file

@ -0,0 +1,163 @@
import {
User,
GuildMember,
GuildChannel,
EmbedField,
PermissionsBitField,
} from 'discord.js';
import { LogActionPayload, LogActionType, RoleProperties } from './types.js';
import { ACTION_EMOJIS } from './constants.js';
export const formatPermissionName = (perm: string): string => {
return perm
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
export const createUserField = (
user: User | GuildMember,
label = 'User',
): EmbedField => ({
name: label,
value: `<@${user.id}>`,
inline: true,
});
export const createModeratorField = (
moderator?: GuildMember,
label = 'Moderator',
): EmbedField | null =>
moderator
? {
name: label,
value: `<@${moderator.id}>`,
inline: true,
}
: null;
export const createChannelField = (channel: GuildChannel): EmbedField => ({
name: 'Channel',
value: `<#${channel.id}>`,
inline: true,
});
export const createPermissionChangeFields = (
oldPerms: Readonly<PermissionsBitField>,
newPerms: Readonly<PermissionsBitField>,
): EmbedField[] => {
const fields: EmbedField[] = [];
const changes: { added: string[]; removed: string[] } = {
added: [],
removed: [],
};
Object.keys(PermissionsBitField.Flags).forEach((perm) => {
const hasOld = oldPerms.has(perm as keyof typeof PermissionsBitField.Flags);
const hasNew = newPerms.has(perm as keyof typeof PermissionsBitField.Flags);
if (hasOld !== hasNew) {
if (hasNew) {
changes.added.push(formatPermissionName(perm));
} else {
changes.removed.push(formatPermissionName(perm));
}
}
});
if (changes.added.length) {
fields.push({
name: '✅ Added Permissions',
value: changes.added.join('\n'),
inline: true,
});
}
if (changes.removed.length) {
fields.push({
name: '❌ Removed Permissions',
value: changes.removed.join('\n'),
inline: true,
});
}
return fields;
};
export const createRoleChangeFields = (
oldRole: Partial<RoleProperties>,
newRole: Partial<RoleProperties>,
): EmbedField[] => {
const fields: EmbedField[] = [];
if (oldRole.name !== newRole.name) {
fields.push({
name: 'Name Changed',
value: `${oldRole.name}${newRole.name}`,
inline: true,
});
}
if (oldRole.color !== newRole.color) {
fields.push({
name: 'Color Changed',
value: `${oldRole.color || 'None'}${newRole.color || 'None'}`,
inline: true,
});
}
const booleanProps: Array<
keyof Pick<RoleProperties, 'hoist' | 'mentionable'>
> = ['hoist', 'mentionable'];
for (const prop of booleanProps) {
if (oldRole[prop] !== newRole[prop]) {
fields.push({
name: `${prop.charAt(0).toUpperCase() + prop.slice(1)} Changed`,
value: `${oldRole[prop] ? 'Yes' : 'No'}${newRole[prop] ? 'Yes' : 'No'}`,
inline: true,
});
}
}
return fields;
};
export const getLogItemId = (payload: LogActionPayload): string => {
switch (payload.action) {
case 'roleCreate':
case 'roleDelete':
case 'roleUpdate':
case 'roleAdd':
case 'roleRemove':
return payload.role.id;
case 'channelCreate':
case 'channelDelete':
case 'channelUpdate':
return payload.channel.id;
case 'messageDelete':
case 'messageEdit':
return payload.message.id;
case 'memberJoin':
case 'memberLeave':
return payload.member.id;
case 'ban':
case 'kick':
case 'mute':
case 'unban':
case 'unmute':
case 'warn':
return payload.target.id;
default:
return 'N/A';
}
};
export const getEmojiForAction = (action: LogActionType): string => {
return ACTION_EMOJIS[action] || '📝';
};