feat: detect reply/forward/crosspost in message capture + inject into AI moderation

- Add is_reply, is_forward, is_crosspost, reference_message_id,
  reference_channel_id, reference_guild_id columns to messages table
- Update MessageRecord type with reference fields
- Extract reply/forward/crosspost from Discord message type/flags
- Track reference.type (DEFAULT=reply, FORWARD) and CROSSPOSTED flag
- Inject <reference> XML with parent content into LLM moderation prompt
- Add reply/forward/crosspost rules to moderation prompt rules
- Format context messages with [reply_to], [forward_from], [crosspost]
- Add migration 0009

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-13 00:55:42 +07:00
co-authored by Claude
parent 0ac056dc8a
commit e3249edb6c
9 changed files with 133 additions and 8 deletions
@@ -94,6 +94,20 @@ function buildMessageRecord(
const location = getMessageLocation(message);
const metadata = getMessageMetadata(message);
const guildId = requireMessageGuildId(message);
const ref = message.reference;
// is_reply: type === 'REPLY' OR reference type === 'DEFAULT'
// is_forward: reference type === 'FORWARD'
// is_crosspost: message flags has CROSSPOSTED
const msgType = message.type as string;
const refType = (ref?.type as string | undefined) ?? null;
const isReply =
msgType === "REPLY" || (refType === "DEFAULT" && msgType !== "FORWARD")
? true
: null;
const isForward = refType === "FORWARD" ? true : null;
const isCrosspost =
message.flags?.has(1 << 1) || msgType === "CROSSPOSTED" ? true : null;
return {
id: message.id,
@@ -109,6 +123,12 @@ function buildMessageRecord(
edited_at: null,
deleted_at: null,
type,
is_reply: isReply,
is_forward: isForward,
is_crosspost: isCrosspost,
reference_message_id: ref?.messageId ?? null,
reference_channel_id: ref?.channelId ?? null,
reference_guild_id: ref?.guildId ?? null,
metadata: JSON.stringify(metadata),
};
}
@@ -81,7 +81,9 @@ export interface RichMessageMetadata {
messageId: string | null;
channelId: string | null;
guildId: string | null;
type: string | null;
} | null;
isCrosspost: boolean;
}
export function getMessageLocation(message: Message): MessageLocation {
@@ -235,8 +237,10 @@ export function getMessageMetadata(message: Message): RichMessageMetadata {
messageId: message.reference.messageId ?? null,
channelId: message.reference.channelId ?? null,
guildId: message.reference.guildId ?? null,
type: (message.reference.type as unknown as string | undefined) ?? null,
}
: null,
isCrosspost: message.flags?.has(1 << 1) ?? false,
};
}
@@ -258,6 +262,7 @@ export function parseRichMessageMetadata(
member: (parsed.member ?? null) as RichMessageMetadata["member"],
channel: parsed.channel as RichMessageMetadata["channel"],
reference: (parsed.reference ?? null) as RichMessageMetadata["reference"],
isCrosspost: Boolean(parsed.isCrosspost),
};
} catch {
return null;