feat(gateway): capture NSFW/age-restricted messages but skip AI analysis + exclude from public archive

Previously isAgeRestrictedMessage() early-returned in messageCreate/messageUpdate,
so NSFW-channel messages were never stored at all. Now they are captured like
any message (visible in dashboard), while the existing age-restricted skip path
(queueMessageAnalysis -> buildAgeRestrictedSkipResult) marks them clean with flag
age_restricted WITHOUT calling the LLM.

NSFW content is also deliberately kept OUT of the Qdrant public semantic-search
archive (archiveMessageEmbedded skips when isAgeRestricted), so it can't be found
via public web search. No schema change needed (metadata already carries channel.nsfw).
This commit is contained in:
asepharyana
2026-08-30 20:13:35 +07:00
committed by asepharyana
parent 7dfb4035b7
commit 50967f6481
3 changed files with 65 additions and 3 deletions
@@ -16,6 +16,10 @@ export interface ArchiveMessage {
channel_id: string;
guild_id: string;
created_at: number;
/** True when the message came from an age-restricted (NSFW) channel. NSFW
* content is deliberately NOT embedded into the public archive so it can't
* be found via public semantic search. Defaults to false. */
isAgeRestricted?: boolean;
}
/**
@@ -25,8 +29,12 @@ export interface ArchiveMessage {
* Failures are swallowed — searching is a nice-to-have, never a precondition
* for capture or moderation. The message text is kept in the payload so the
* search endpoint can return results even for deleted messages.
*
* NSFW/age-restricted messages are skipped (never embedded) — they are stored
* in the database for the dashboard but kept out of the public search archive.
*/
export function archiveMessageEmbedded(message: ArchiveMessage): void {
if (message.isAgeRestricted) return; // never surface NSFW in public archive
if (!config.AI_LLM_EMBEDDING_MODEL) return; // embeddings disabled → skip
const text = message.content?.trim();
if (!text || text.length < 3) return;
@@ -215,8 +215,12 @@ export async function captureMessage(
// Fire-and-forget: make the captured message searchable in the persistent
// archive (public semantic search). Never blocks capture/moderation.
// NSFW/age-restricted messages are kept OUT of the public archive.
if (!isBacklog && messageRecord.content) {
archiveMessageEmbedded(messageRecord);
archiveMessageEmbedded({
...messageRecord,
isAgeRestricted: isAgeRestrictedMessage(message),
});
}
if (_eventBroadcaster && !isBacklog) {
@@ -288,7 +292,6 @@ export function registerMessageCapture(client: Client): void {
client.on("messageCreate", async (message) => {
if (!shouldCaptureForAnyTarget(message, targets)) return;
if (isBotExcludedChannel(message)) return;
if (isAgeRestrictedMessage(message)) return;
if (isExcludedThread(message)) return;
try {
@@ -307,7 +310,6 @@ export function registerMessageCapture(client: Client): void {
client.on("messageUpdate", async (_oldMessage, newMessage) => {
if (!shouldCaptureForAnyTarget(newMessage, targets)) return;
if (isBotExcludedChannel(newMessage as Message)) return;
if (isAgeRestrictedMessage(newMessage as Message)) return;
if (isExcludedThread(newMessage)) return;
try {