From 701ab8caa673904778535cc40b002e4b5ef8a354 Mon Sep 17 00:00:00 2001 From: seeyebe Date: Fri, 26 Sep 2025 22:19:59 +0300 Subject: [PATCH 1/2] feat: add message pin and unpin log --- backend/src/data/DefaultLogMessages.json | 2 + backend/src/data/GuildSavedMessages.ts | 2 + backend/src/data/LogType.ts | 2 + backend/src/data/entities/SavedMessage.ts | 1 + backend/src/plugins/Logs/LogsPlugin.ts | 4 + .../Logs/logFunctions/logMessagePin.ts | 46 ++++++++++++ .../Logs/logFunctions/logMessageUnpin.ts | 46 ++++++++++++ backend/src/plugins/Logs/types.ts | 14 ++++ .../src/plugins/Logs/util/onMessageUpdate.ts | 74 ++++++++++++++++--- backend/src/utils/templateSafeObjects.ts | 2 + 10 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 backend/src/plugins/Logs/logFunctions/logMessagePin.ts create mode 100644 backend/src/plugins/Logs/logFunctions/logMessageUnpin.ts diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index ff0d12cae..c371d6ab2 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -39,6 +39,8 @@ "MESSAGE_DELETE_BULK": "{timestamp} 🗑 **{count}** messages by {authorIds} deleted in {channelMention(channel)} ({archiveUrl})", "MESSAGE_DELETE_BARE": "{timestamp} 🗑 Message (`{messageId}`) deleted in {channelMention(channel)} (no more info available)", "MESSAGE_DELETE_AUTO": "{timestamp} 🗑 Auto-deleted message (`{message.id}`) from {userMention(user)} in {channelMention(channel)} (originally posted at **{messageDate}**):{messageSummary(message)}", + "MESSAGE_PIN": "{timestamp} 📌 {userMention(mod)} pinned a message by {userMention(user)} in {channelMention(channel)}:{messageSummary(message)}", + "MESSAGE_UNPIN": "{timestamp} 📌 {userMention(mod)} unpinned a message by {userMention(user)} in {channelMention(channel)}:{messageSummary(message)}", "VOICE_CHANNEL_JOIN": "{timestamp} 🎙 🔵 {userMention(member)} joined {channelMention(channel)}", "VOICE_CHANNEL_MOVE": "{timestamp} 🎙 ↔ {userMention(member)} moved from {channelMention(oldChannel)} to {channelMention(newChannel)}", diff --git a/backend/src/data/GuildSavedMessages.ts b/backend/src/data/GuildSavedMessages.ts index 3edc28104..c340965a1 100644 --- a/backend/src/data/GuildSavedMessages.ts +++ b/backend/src/data/GuildSavedMessages.ts @@ -119,6 +119,8 @@ export class GuildSavedMessages extends BaseGuildRepository { })); } + data.pinned = msg.pinned; + return data; } diff --git a/backend/src/data/LogType.ts b/backend/src/data/LogType.ts index 1aba45ec0..adad69332 100644 --- a/backend/src/data/LogType.ts +++ b/backend/src/data/LogType.ts @@ -28,6 +28,8 @@ export const LogType = { MESSAGE_DELETE: "MESSAGE_DELETE", MESSAGE_DELETE_BULK: "MESSAGE_DELETE_BULK", MESSAGE_DELETE_BARE: "MESSAGE_DELETE_BARE", + MESSAGE_PIN: "MESSAGE_PIN", + MESSAGE_UNPIN: "MESSAGE_UNPIN", VOICE_CHANNEL_JOIN: "VOICE_CHANNEL_JOIN", VOICE_CHANNEL_LEAVE: "VOICE_CHANNEL_LEAVE", VOICE_CHANNEL_MOVE: "VOICE_CHANNEL_MOVE", diff --git a/backend/src/data/entities/SavedMessage.ts b/backend/src/data/entities/SavedMessage.ts index 9a0b310ff..c7fc835fc 100644 --- a/backend/src/data/entities/SavedMessage.ts +++ b/backend/src/data/entities/SavedMessage.ts @@ -72,6 +72,7 @@ export interface ISavedMessageData { discriminator: string; }; content: string; + pinned?: boolean; embeds?: ISavedMessageEmbedData[]; stickers?: ISavedMessageStickerData[]; timestamp: number; diff --git a/backend/src/plugins/Logs/LogsPlugin.ts b/backend/src/plugins/Logs/LogsPlugin.ts index 0485351c5..32ee96b7f 100644 --- a/backend/src/plugins/Logs/LogsPlugin.ts +++ b/backend/src/plugins/Logs/LogsPlugin.ts @@ -82,6 +82,8 @@ import { logMessageDelete } from "./logFunctions/logMessageDelete.js"; import { logMessageDeleteAuto } from "./logFunctions/logMessageDeleteAuto.js"; import { logMessageDeleteBare } from "./logFunctions/logMessageDeleteBare.js"; import { logMessageDeleteBulk } from "./logFunctions/logMessageDeleteBulk.js"; +import { logMessagePin } from "./logFunctions/logMessagePin.js"; +import { logMessageUnpin } from "./logFunctions/logMessageUnpin.js"; import { logMessageEdit } from "./logFunctions/logMessageEdit.js"; import { logMessageSpamDetected } from "./logFunctions/logMessageSpamDetected.js"; import { logOtherSpamDetected } from "./logFunctions/logOtherSpamDetected.js"; @@ -200,6 +202,8 @@ export const LogsPlugin = guildPlugin()({ logMessageDeleteAuto: makePublicFn(pluginData, logMessageDeleteAuto), logMessageDeleteBare: makePublicFn(pluginData, logMessageDeleteBare), logMessageDeleteBulk: makePublicFn(pluginData, logMessageDeleteBulk), + logMessagePin: makePublicFn(pluginData, logMessagePin), + logMessageUnpin: makePublicFn(pluginData, logMessageUnpin), logMessageEdit: makePublicFn(pluginData, logMessageEdit), logMessageSpamDetected: makePublicFn(pluginData, logMessageSpamDetected), logOtherSpamDetected: makePublicFn(pluginData, logOtherSpamDetected), diff --git a/backend/src/plugins/Logs/logFunctions/logMessagePin.ts b/backend/src/plugins/Logs/logFunctions/logMessagePin.ts new file mode 100644 index 000000000..9f6ed34f3 --- /dev/null +++ b/backend/src/plugins/Logs/logFunctions/logMessagePin.ts @@ -0,0 +1,46 @@ +import { GuildTextBasedChannel, User } from "discord.js"; +import { GuildPluginData } from "knub"; +import { LogType } from "../../../data/LogType.js"; +import { ISavedMessageAttachmentData, SavedMessage } from "../../../data/entities/SavedMessage.js"; +import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter.js"; +import { UnknownUser, useMediaUrls } from "../../../utils.js"; +import { resolveChannelIds } from "../../../utils/resolveChannelIds.js"; +import { + channelToTemplateSafeChannel, + savedMessageToTemplateSafeSavedMessage, + userToTemplateSafeUser, +} from "../../../utils/templateSafeObjects.js"; +import { LogsPluginType } from "../types.js"; +import { log } from "../util/log.js"; + +export interface LogMessagePinData { + mod: User | UnknownUser | null; + user: User | UnknownUser; + channel: GuildTextBasedChannel; + message: SavedMessage; +} + +export function logMessagePin(pluginData: GuildPluginData, data: LogMessagePinData) { + if (data.message.data.attachments) { + for (const attachment of data.message.data.attachments as ISavedMessageAttachmentData[]) { + attachment.url = useMediaUrls(attachment.url); + } + } + + return log( + pluginData, + LogType.MESSAGE_PIN, + createTypedTemplateSafeValueContainer({ + mod: data.mod ? userToTemplateSafeUser(data.mod) : null, + user: userToTemplateSafeUser(data.user), + channel: channelToTemplateSafeChannel(data.channel), + message: savedMessageToTemplateSafeSavedMessage(data.message), + }), + { + userId: data.user.id, + messageTextContent: data.message.data.content, + bot: data.user instanceof User ? data.user.bot : false, + ...resolveChannelIds(data.channel), + }, + ); +} diff --git a/backend/src/plugins/Logs/logFunctions/logMessageUnpin.ts b/backend/src/plugins/Logs/logFunctions/logMessageUnpin.ts new file mode 100644 index 000000000..a1ec7409d --- /dev/null +++ b/backend/src/plugins/Logs/logFunctions/logMessageUnpin.ts @@ -0,0 +1,46 @@ +import { GuildTextBasedChannel, User } from "discord.js"; +import { GuildPluginData } from "knub"; +import { LogType } from "../../../data/LogType.js"; +import { ISavedMessageAttachmentData, SavedMessage } from "../../../data/entities/SavedMessage.js"; +import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter.js"; +import { UnknownUser, useMediaUrls } from "../../../utils.js"; +import { resolveChannelIds } from "../../../utils/resolveChannelIds.js"; +import { + channelToTemplateSafeChannel, + savedMessageToTemplateSafeSavedMessage, + userToTemplateSafeUser, +} from "../../../utils/templateSafeObjects.js"; +import { LogsPluginType } from "../types.js"; +import { log } from "../util/log.js"; + +export interface LogMessageUnpinData { + mod: User | UnknownUser | null; + user: User | UnknownUser; + channel: GuildTextBasedChannel; + message: SavedMessage; +} + +export function logMessageUnpin(pluginData: GuildPluginData, data: LogMessageUnpinData) { + if (data.message.data.attachments) { + for (const attachment of data.message.data.attachments as ISavedMessageAttachmentData[]) { + attachment.url = useMediaUrls(attachment.url); + } + } + + return log( + pluginData, + LogType.MESSAGE_UNPIN, + createTypedTemplateSafeValueContainer({ + mod: data.mod ? userToTemplateSafeUser(data.mod) : null, + user: userToTemplateSafeUser(data.user), + channel: channelToTemplateSafeChannel(data.channel), + message: savedMessageToTemplateSafeSavedMessage(data.message), + }), + { + userId: data.user.id, + messageTextContent: data.message.data.content, + bot: data.user instanceof User ? data.user.bot : false, + ...resolveChannelIds(data.channel), + }, + ); +} diff --git a/backend/src/plugins/Logs/types.ts b/backend/src/plugins/Logs/types.ts index 1efbbc628..cfff4d5ca 100644 --- a/backend/src/plugins/Logs/types.ts +++ b/backend/src/plugins/Logs/types.ts @@ -250,6 +250,20 @@ export const LogTypeData = z.object({ channel: z.instanceof(TemplateSafeChannel), }), + [LogType.MESSAGE_PIN]: z.object({ + mod: z.instanceof(TemplateSafeUser).or(z.null()), + user: z.instanceof(TemplateSafeUser), + channel: z.instanceof(TemplateSafeChannel), + message: z.instanceof(TemplateSafeSavedMessage), + }), + + [LogType.MESSAGE_UNPIN]: z.object({ + mod: z.instanceof(TemplateSafeUser).or(z.null()), + user: z.instanceof(TemplateSafeUser), + channel: z.instanceof(TemplateSafeChannel), + message: z.instanceof(TemplateSafeSavedMessage), + }), + [LogType.VOICE_CHANNEL_JOIN]: z.object({ member: z.instanceof(TemplateSafeMember), channel: z.instanceof(TemplateSafeChannel), diff --git a/backend/src/plugins/Logs/util/onMessageUpdate.ts b/backend/src/plugins/Logs/util/onMessageUpdate.ts index ecc9a5f12..144fcb7c3 100644 --- a/backend/src/plugins/Logs/util/onMessageUpdate.ts +++ b/backend/src/plugins/Logs/util/onMessageUpdate.ts @@ -1,9 +1,14 @@ -import { EmbedData, GuildTextBasedChannel, Snowflake } from "discord.js"; +import { AuditLogEvent, EmbedData, GuildTextBasedChannel, Snowflake, User } from "discord.js"; import { GuildPluginData } from "knub"; +import { LogType } from "../../../data/LogType.js"; import { SavedMessage } from "../../../data/entities/SavedMessage.js"; -import { resolveUser } from "../../../utils.js"; +import { findMatchingAuditLogEntry } from "../../../utils/findMatchingAuditLogEntry.js"; +import { resolveUser, UnknownUser } from "../../../utils.js"; import { logMessageEdit } from "../logFunctions/logMessageEdit.js"; +import { logMessagePin } from "../logFunctions/logMessagePin.js"; +import { logMessageUnpin } from "../logFunctions/logMessageUnpin.js"; import { LogsPluginType } from "../types.js"; +import { isLogIgnored } from "./isLogIgnored.js"; export async function onMessageUpdate( pluginData: GuildPluginData, @@ -41,17 +46,64 @@ export async function onMessageUpdate( logUpdate = true; } - if (!logUpdate) { + const wasPinned = oldSavedMessage.data.pinned ?? false; + const isPinned = savedMessage.data.pinned ?? false; + const pinStateChanged = wasPinned !== isPinned; + + if (!logUpdate && !pinStateChanged) { return; } const user = await resolveUser(pluginData.client, savedMessage.user_id); - const channel = pluginData.guild.channels.resolve(savedMessage.channel_id as Snowflake)! as GuildTextBasedChannel; - - logMessageEdit(pluginData, { - user, - channel, - before: oldSavedMessage, - after: savedMessage, - }); + const resolvedChannel = pluginData.guild.channels.resolve(savedMessage.channel_id as Snowflake); + if (!resolvedChannel || !resolvedChannel.isTextBased()) { + return; + } + const channel = resolvedChannel as GuildTextBasedChannel; + + if (logUpdate) { + logMessageEdit(pluginData, { + user, + channel, + before: oldSavedMessage, + after: savedMessage, + }); + } + + if (pinStateChanged) { + const logType = isPinned ? LogType.MESSAGE_PIN : LogType.MESSAGE_UNPIN; + if (!isLogIgnored(pluginData, logType, savedMessage.id)) { + const auditLogAction = isPinned ? AuditLogEvent.MessagePin : AuditLogEvent.MessageUnpin; + const relevantAuditLogEntry = await findMatchingAuditLogEntry( + pluginData.guild, + auditLogAction, + savedMessage.user_id, + ); + + let mod: User | UnknownUser | null = null; + let skipMod = false; + + if (relevantAuditLogEntry?.extra) { + const extra: any = relevantAuditLogEntry.extra; + if ( + (extra?.channel?.id && extra.channel.id !== savedMessage.channel_id) || + (extra?.messageId && extra.messageId !== savedMessage.id) + ) { + skipMod = true; + } + } + + if (!skipMod && relevantAuditLogEntry?.executor?.id) { + mod = await resolveUser(pluginData.client, relevantAuditLogEntry.executor.id); + } + + const logFn = isPinned ? logMessagePin : logMessageUnpin; + logFn(pluginData, { + mod, + user, + channel, + message: savedMessage, + }); + } + } } diff --git a/backend/src/utils/templateSafeObjects.ts b/backend/src/utils/templateSafeObjects.ts index 196593f3a..806138e1d 100644 --- a/backend/src/utils/templateSafeObjects.ts +++ b/backend/src/utils/templateSafeObjects.ts @@ -188,6 +188,7 @@ export class TemplateSafeSavedMessageData extends TemplateSafeValueContainer { discriminator: string; }>; content: string; + pinned?: boolean; embeds?: Array>; stickers?: Array>; timestamp: number; @@ -445,6 +446,7 @@ export function savedMessageToTemplateSafeSavedMessage(savedMessage: SavedMessag ), timestamp: savedMessage.data.timestamp, + pinned: savedMessage.data.pinned ?? false, }), }); } From d567f8442eee43a1eb956aad5efe283a4de67ae0 Mon Sep 17 00:00:00 2001 From: seeyebe <85168740+seeyebe@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:08:46 +0200 Subject: [PATCH 2/2] fix: add missing calls --- .../src/plugins/Logs/util/onMessageUpdate.ts | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/backend/src/plugins/Logs/util/onMessageUpdate.ts b/backend/src/plugins/Logs/util/onMessageUpdate.ts index 826b46b15..6c83842cb 100644 --- a/backend/src/plugins/Logs/util/onMessageUpdate.ts +++ b/backend/src/plugins/Logs/util/onMessageUpdate.ts @@ -1,9 +1,14 @@ -import { EmbedData, GuildTextBasedChannel, Snowflake } from "discord.js"; +import { AuditLogEvent, EmbedData, GuildTextBasedChannel, Snowflake, User } from "discord.js"; import { GuildPluginData } from "vety"; +import { LogType } from "../../../data/LogType.js"; import { SavedMessage } from "../../../data/entities/SavedMessage.js"; -import { resolveUser } from "../../../utils.js"; +import { findMatchingAuditLogEntry } from "../../../utils/findMatchingAuditLogEntry.js"; +import { resolveUser, UnknownUser } from "../../../utils.js"; import { logMessageEdit } from "../logFunctions/logMessageEdit.js"; +import { logMessagePin } from "../logFunctions/logMessagePin.js"; +import { logMessageUnpin } from "../logFunctions/logMessageUnpin.js"; import { LogsPluginType } from "../types.js"; +import { isLogIgnored } from "./isLogIgnored.js"; export async function onMessageUpdate( pluginData: GuildPluginData, @@ -41,17 +46,64 @@ export async function onMessageUpdate( logUpdate = true; } - if (!logUpdate) { + const wasPinned = oldSavedMessage.data.pinned ?? false; + const isPinned = savedMessage.data.pinned ?? false; + const pinStateChanged = wasPinned !== isPinned; + + if (!logUpdate && !pinStateChanged) { + return; + } + + const user = await resolveUser(pluginData.client, savedMessage.user_id); + const resolvedChannel = pluginData.guild.channels.resolve(savedMessage.channel_id as Snowflake); + if (!resolvedChannel || !resolvedChannel.isTextBased()) { return; } + const channel = resolvedChannel as GuildTextBasedChannel; + + if (logUpdate) { + logMessageEdit(pluginData, { + user, + channel, + before: oldSavedMessage, + after: savedMessage, + }); + } - const user = await resolveUser(pluginData.client, savedMessage.user_id, "Logs:onMessageUpdate"); - const channel = pluginData.guild.channels.resolve(savedMessage.channel_id as Snowflake)! as GuildTextBasedChannel; + if (pinStateChanged) { + const logType = isPinned ? LogType.MESSAGE_PIN : LogType.MESSAGE_UNPIN; + if (!isLogIgnored(pluginData, logType, savedMessage.id)) { + const auditLogAction = isPinned ? AuditLogEvent.MessagePin : AuditLogEvent.MessageUnpin; + const relevantAuditLogEntry = await findMatchingAuditLogEntry( + pluginData.guild, + auditLogAction, + savedMessage.user_id, + ); - logMessageEdit(pluginData, { - user, - channel, - before: oldSavedMessage, - after: savedMessage, - }); -} \ No newline at end of file + let mod: User | UnknownUser | null = null; + let skipMod = false; + + if (relevantAuditLogEntry?.extra) { + const extra: any = relevantAuditLogEntry.extra; + if ( + (extra?.channel?.id && extra.channel.id !== savedMessage.channel_id) || + (extra?.messageId && extra.messageId !== savedMessage.id) + ) { + skipMod = true; + } + } + + if (!skipMod && relevantAuditLogEntry?.executor?.id) { + mod = await resolveUser(pluginData.client, relevantAuditLogEntry.executor.id); + } + + const logFn = isPinned ? logMessagePin : logMessageUnpin; + logFn(pluginData, { + mod, + user, + channel, + message: savedMessage, + }); + } + } +}