Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/discord-bot/src/features/DiscordQueuedPromptRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { MessageId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { createDiscordQueuedPromptRegistry } from "./DiscordQueuedPromptRegistry.ts";

describe("DiscordQueuedPromptRegistry", () => {
it("remembers entries by Discord message and thread", () => {
const registry = createDiscordQueuedPromptRegistry();
const entry = {
discordChannelId: "channel-1",
discordMessageId: "discord-1",
t3ThreadId: ThreadId.make("thread-1"),
t3MessageId: MessageId.make("msg-1"),
authorUserId: "user-1",
};
registry.remember(entry);
expect(registry.getByDiscordMessageId("discord-1")).toEqual(entry);
expect(registry.listForThread(ThreadId.make("thread-1"))).toEqual([entry]);
});

it("forgets by Discord message id and by T3 message id", () => {
const registry = createDiscordQueuedPromptRegistry();
registry.remember({
discordChannelId: "channel-1",
discordMessageId: "discord-1",
t3ThreadId: ThreadId.make("thread-1"),
t3MessageId: MessageId.make("msg-1"),
authorUserId: null,
});
registry.remember({
discordChannelId: "channel-1",
discordMessageId: "discord-2",
t3ThreadId: ThreadId.make("thread-1"),
t3MessageId: MessageId.make("msg-2"),
authorUserId: null,
});

expect(registry.forgetDiscordMessage("discord-1")?.t3MessageId).toBe("msg-1");
expect(registry.getByDiscordMessageId("discord-1")).toBeNull();
expect(registry.listForThread(ThreadId.make("thread-1"))).toHaveLength(1);

expect(
registry.forgetT3Message(ThreadId.make("thread-1"), MessageId.make("msg-2"))
?.discordMessageId,
).toBe("discord-2");
expect(registry.listForThread(ThreadId.make("thread-1"))).toEqual([]);
});

it("clears an entire thread", () => {
const registry = createDiscordQueuedPromptRegistry();
registry.remember({
discordChannelId: "channel-1",
discordMessageId: "discord-1",
t3ThreadId: ThreadId.make("thread-1"),
t3MessageId: MessageId.make("msg-1"),
authorUserId: null,
});
const cleared = registry.clearThread(ThreadId.make("thread-1"));
expect(cleared).toHaveLength(1);
expect(registry.listForThread(ThreadId.make("thread-1"))).toEqual([]);
});
});
93 changes: 93 additions & 0 deletions apps/discord-bot/src/features/DiscordQueuedPromptRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { MessageId, ThreadId } from "@t3tools/contracts";

/**
* Tracks Discord user messages that are parked in the server follow-up queue so
* we can badge them (📥), remove on delete, and flush via /omegent steernow.
*
* In-memory only: process restart drops badges (server queue remains authoritative).
*/
export type PendingQueuedPrompt = {
readonly discordChannelId: string;
readonly discordMessageId: string;
readonly t3ThreadId: ThreadId;
readonly t3MessageId: MessageId;
readonly authorUserId: string | null;
};

/** Discord unicode used as the "queued" badge on the user's message. */
export const QUEUED_PROMPT_REACTION_EMOJI = "📥";

export function createDiscordQueuedPromptRegistry() {
const byDiscordMessageId = new Map<string, PendingQueuedPrompt>();
const byT3ThreadId = new Map<string, Set<string>>();

const remember = (entry: PendingQueuedPrompt): void => {
byDiscordMessageId.set(entry.discordMessageId, entry);
const key = String(entry.t3ThreadId);
const set = byT3ThreadId.get(key) ?? new Set<string>();
set.add(entry.discordMessageId);
byT3ThreadId.set(key, set);
};

const forgetDiscordMessage = (discordMessageId: string): PendingQueuedPrompt | null => {
const entry = byDiscordMessageId.get(discordMessageId);
if (entry === undefined) return null;
byDiscordMessageId.delete(discordMessageId);
const key = String(entry.t3ThreadId);
const set = byT3ThreadId.get(key);
if (set !== undefined) {
set.delete(discordMessageId);
if (set.size === 0) byT3ThreadId.delete(key);
}
return entry;
};

const forgetT3Message = (
t3ThreadId: ThreadId,
t3MessageId: MessageId,
): PendingQueuedPrompt | null => {
const key = String(t3ThreadId);
const set = byT3ThreadId.get(key);
if (set === undefined) return null;
for (const discordMessageId of set) {
const entry = byDiscordMessageId.get(discordMessageId);
if (entry !== undefined && entry.t3MessageId === t3MessageId) {
return forgetDiscordMessage(discordMessageId);
}
}
return null;
};

const listForThread = (t3ThreadId: ThreadId): ReadonlyArray<PendingQueuedPrompt> => {
const set = byT3ThreadId.get(String(t3ThreadId));
if (set === undefined) return [];
const out: PendingQueuedPrompt[] = [];
for (const discordMessageId of set) {
const entry = byDiscordMessageId.get(discordMessageId);
if (entry !== undefined) out.push(entry);
}
return out;
};

const getByDiscordMessageId = (discordMessageId: string): PendingQueuedPrompt | null =>
byDiscordMessageId.get(discordMessageId) ?? null;

const clearThread = (t3ThreadId: ThreadId): ReadonlyArray<PendingQueuedPrompt> => {
const listed = listForThread(t3ThreadId);
for (const entry of listed) {
forgetDiscordMessage(entry.discordMessageId);
}
return listed;
};

return {
remember,
forgetDiscordMessage,
forgetT3Message,
listForThread,
getByDiscordMessageId,
clearThread,
};
}

export type DiscordQueuedPromptRegistry = ReturnType<typeof createDiscordQueuedPromptRegistry>;
Loading
Loading