From 30d091b2fb2e37f7fe967b78403dbdcb1287a350 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 20:04:44 +0000 Subject: [PATCH] refactor: share thread binding id parser --- src/channels/thread-binding-id.test.ts | 43 +++++++++++++++++++ src/channels/thread-binding-id.ts | 15 +++++++ .../monitor/thread-bindings.manager.ts | 24 +++-------- src/telegram/thread-bindings.ts | 21 ++------- 4 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 src/channels/thread-binding-id.test.ts create mode 100644 src/channels/thread-binding-id.ts diff --git a/src/channels/thread-binding-id.test.ts b/src/channels/thread-binding-id.test.ts new file mode 100644 index 00000000000..ad336b291bb --- /dev/null +++ b/src/channels/thread-binding-id.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { resolveThreadBindingConversationIdFromBindingId } from "./thread-binding-id.js"; + +describe("resolveThreadBindingConversationIdFromBindingId", () => { + it("returns the conversation id for matching account-prefixed binding ids", () => { + expect( + resolveThreadBindingConversationIdFromBindingId({ + accountId: "default", + bindingId: "default:thread-123", + }), + ).toBe("thread-123"); + }); + + it("returns undefined when binding id is missing or account prefix does not match", () => { + expect( + resolveThreadBindingConversationIdFromBindingId({ + accountId: "default", + bindingId: undefined, + }), + ).toBeUndefined(); + expect( + resolveThreadBindingConversationIdFromBindingId({ + accountId: "default", + bindingId: "work:thread-123", + }), + ).toBeUndefined(); + }); + + it("trims whitespace and rejects empty ids after the account prefix", () => { + expect( + resolveThreadBindingConversationIdFromBindingId({ + accountId: "default", + bindingId: " default:group-1:topic:99 ", + }), + ).toBe("group-1:topic:99"); + expect( + resolveThreadBindingConversationIdFromBindingId({ + accountId: "default", + bindingId: "default: ", + }), + ).toBeUndefined(); + }); +}); diff --git a/src/channels/thread-binding-id.ts b/src/channels/thread-binding-id.ts new file mode 100644 index 00000000000..c9db30e3637 --- /dev/null +++ b/src/channels/thread-binding-id.ts @@ -0,0 +1,15 @@ +export function resolveThreadBindingConversationIdFromBindingId(params: { + accountId: string; + bindingId?: string; +}): string | undefined { + const bindingId = params.bindingId?.trim(); + if (!bindingId) { + return undefined; + } + const prefix = `${params.accountId}:`; + if (!bindingId.startsWith(prefix)) { + return undefined; + } + const conversationId = bindingId.slice(prefix.length).trim(); + return conversationId || undefined; +} diff --git a/src/discord/monitor/thread-bindings.manager.ts b/src/discord/monitor/thread-bindings.manager.ts index 9592962f368..386d1adbc8c 100644 --- a/src/discord/monitor/thread-bindings.manager.ts +++ b/src/discord/monitor/thread-bindings.manager.ts @@ -1,4 +1,5 @@ import { Routes } from "discord-api-types/v10"; +import { resolveThreadBindingConversationIdFromBindingId } from "../../channels/thread-binding-id.js"; import { logVerbose } from "../../globals.js"; import { registerSessionBindingAdapter, @@ -157,22 +158,6 @@ function toSessionBindingRecord( }; } -function resolveThreadIdFromBindingId(params: { - accountId: string; - bindingId?: string; -}): string | undefined { - const bindingId = params.bindingId?.trim(); - if (!bindingId) { - return undefined; - } - const prefix = `${params.accountId}:`; - if (!bindingId.startsWith(prefix)) { - return undefined; - } - const threadId = bindingId.slice(prefix.length).trim(); - return threadId || undefined; -} - export function createThreadBindingManager( params: { accountId?: string; @@ -617,7 +602,10 @@ export function createThreadBindingManager( return binding ? toSessionBindingRecord(binding, { idleTimeoutMs, maxAgeMs }) : null; }, touch: (bindingId, at) => { - const threadId = resolveThreadIdFromBindingId({ accountId, bindingId }); + const threadId = resolveThreadBindingConversationIdFromBindingId({ + accountId, + bindingId, + }); if (!threadId) { return; } @@ -631,7 +619,7 @@ export function createThreadBindingManager( }); return removed.map((entry) => toSessionBindingRecord(entry, { idleTimeoutMs, maxAgeMs })); } - const threadId = resolveThreadIdFromBindingId({ + const threadId = resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId: input.bindingId, }); diff --git a/src/telegram/thread-bindings.ts b/src/telegram/thread-bindings.ts index 3357375b822..68218e9045d 100644 --- a/src/telegram/thread-bindings.ts +++ b/src/telegram/thread-bindings.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { resolveThreadBindingConversationIdFromBindingId } from "../channels/thread-binding-id.js"; import { formatThreadBindingDurationLabel } from "../channels/thread-bindings-messages.js"; import { resolveStateDir } from "../config/paths.js"; import { logVerbose } from "../globals.js"; @@ -312,22 +313,6 @@ async function persistBindingsToDisk(params: { }); } -function resolveThreadIdFromBindingId(params: { - accountId: string; - bindingId?: string; -}): string | undefined { - const bindingId = params.bindingId?.trim(); - if (!bindingId) { - return undefined; - } - const prefix = `${params.accountId}:`; - if (!bindingId.startsWith(prefix)) { - return undefined; - } - const conversationId = bindingId.slice(prefix.length).trim(); - return conversationId || undefined; -} - function normalizeTimestampMs(raw: unknown): number { if (typeof raw !== "number" || !Number.isFinite(raw)) { return Date.now(); @@ -575,7 +560,7 @@ export function createTelegramThreadBindingManager( : null; }, touch: (bindingId, at) => { - const conversationId = resolveThreadIdFromBindingId({ + const conversationId = resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId, }); @@ -598,7 +583,7 @@ export function createTelegramThreadBindingManager( }), ); } - const conversationId = resolveThreadIdFromBindingId({ + const conversationId = resolveThreadBindingConversationIdFromBindingId({ accountId, bindingId: input.bindingId, });