From 897a7b794ff579933cb3a732ffbd74dc0cdd56e3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 00:13:36 +0100 Subject: [PATCH] refactor: dedupe tlon helpers --- extensions/tlon/src/monitor/history.ts | 38 ++++++++------- .../tlon/src/monitor/settings-helpers.test.ts | 46 +++++++----------- extensions/tlon/src/setup-core.ts | 13 +---- extensions/tlon/src/targets.ts | 23 +++++---- extensions/tlon/src/urbit/base-url.test.ts | 23 ++++----- extensions/tlon/src/urbit/sse-client.ts | 47 ++++++++----------- 6 files changed, 80 insertions(+), 110 deletions(-) diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 891af05db19..4ca5c451683 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -27,6 +27,25 @@ export type TlonHistoryEntry = { id?: string; }; +function createHistoryEntryFromMemo(params: { + memo?: Record | null; + seal?: Record | null; + fallbackId?: unknown; +}): TlonHistoryEntry { + const { memo, seal, fallbackId } = params; + return { + author: typeof memo?.author === "string" ? memo.author : "unknown", + content: extractMessageText(memo?.content || []), + timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(), + id: + typeof seal?.id === "string" + ? seal.id + : typeof fallbackId === "string" + ? fallbackId + : undefined, + }; +} + const messageCache = new Map(); const MAX_CACHED_MESSAGES = 100; @@ -166,17 +185,7 @@ export async function fetchThreadHistory( const memo = asRecord(itemRecord?.memo) ?? asRecord(replySet?.memo) ?? itemRecord; const seal = asRecord(itemRecord?.seal) ?? asRecord(replySet?.seal); - return { - author: typeof memo?.author === "string" ? memo.author : "unknown", - content: extractMessageText(memo?.content || []), - timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(), - id: - typeof seal?.id === "string" - ? seal.id - : typeof itemRecord?.id === "string" - ? itemRecord.id - : undefined, - } as TlonHistoryEntry; + return createHistoryEntryFromMemo({ memo, seal, fallbackId: itemRecord?.id }); }) .filter((msg) => msg.content); @@ -202,12 +211,7 @@ export async function fetchThreadHistory( const replyRecord = asRecord(reply); const memo = asRecord(replyRecord?.memo); const seal = asRecord(replyRecord?.seal); - return { - author: typeof memo?.author === "string" ? memo.author : "unknown", - content: extractMessageText(memo?.content || []), - timestamp: typeof memo?.sent === "number" ? memo.sent : Date.now(), - id: typeof seal?.id === "string" ? seal.id : undefined, - }; + return createHistoryEntryFromMemo({ memo, seal }); }) .filter((msg: TlonHistoryEntry) => msg.content); diff --git a/extensions/tlon/src/monitor/settings-helpers.test.ts b/extensions/tlon/src/monitor/settings-helpers.test.ts index c05e8b8ede6..256b5e1038e 100644 --- a/extensions/tlon/src/monitor/settings-helpers.test.ts +++ b/extensions/tlon/src/monitor/settings-helpers.test.ts @@ -26,28 +26,27 @@ const baseAccount: TlonResolvedAccount = { ownerShip: "~marzod", }; +function allowlistMigrationDecisions(currentSettings: Record) { + const allowlistKeys = new Set(["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"]); + return Object.fromEntries( + buildTlonSettingsMigrations(baseAccount, currentSettings) + .filter((migration) => allowlistKeys.has(migration.key)) + .map((migration) => [ + migration.key, + shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue), + ]), + ); +} + describe("shouldMigrateTlonSetting", () => { it("does not rehydrate explicit empty-array revocations during startup migration", () => { - const migrations = buildTlonSettingsMigrations(baseAccount, { + const decisions = allowlistMigrationDecisions({ dmAllowlist: [], groupInviteAllowlist: [], defaultAuthorizedShips: [], }); - expect( - Object.fromEntries( - migrations - .filter((migration) => - ["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes( - migration.key, - ), - ) - .map((migration) => [ - migration.key, - shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue), - ]), - ), - ).toEqual({ + expect(decisions).toEqual({ dmAllowlist: false, groupInviteAllowlist: false, defaultAuthorizedShips: false, @@ -55,22 +54,9 @@ describe("shouldMigrateTlonSetting", () => { }); it("still seeds file-config allowlists on first run when settings are missing", () => { - const migrations = buildTlonSettingsMigrations(baseAccount, {}); + const decisions = allowlistMigrationDecisions({}); - expect( - Object.fromEntries( - migrations - .filter((migration) => - ["dmAllowlist", "groupInviteAllowlist", "defaultAuthorizedShips"].includes( - migration.key, - ), - ) - .map((migration) => [ - migration.key, - shouldMigrateTlonSetting(migration.fileValue, migration.settingsValue), - ]), - ), - ).toEqual({ + expect(decisions).toEqual({ dmAllowlist: true, groupInviteAllowlist: true, defaultAuthorizedShips: true, diff --git a/extensions/tlon/src/setup-core.ts b/extensions/tlon/src/setup-core.ts index dfbfb05f833..92a501db550 100644 --- a/extensions/tlon/src/setup-core.ts +++ b/extensions/tlon/src/setup-core.ts @@ -14,7 +14,7 @@ import { normalizeOptionalString, normalizeStringifiedOptionalString, } from "openclaw/plugin-sdk/text-runtime"; -import { buildTlonAccountFields } from "./account-fields.js"; +import { buildTlonAccountFields, type TlonAccountFieldsInput } from "./account-fields.js"; import { normalizeShip } from "./targets.js"; import { listTlonAccountIds, resolveTlonAccount, type TlonResolvedAccount } from "./types.js"; import { validateUrbitBaseUrl } from "./urbit/base-url.js"; @@ -23,16 +23,7 @@ function tlonChannelId() { return "tlon" as const; } -export type TlonSetupInput = ChannelSetupInput & { - ship?: string; - url?: string; - code?: string; - dangerouslyAllowPrivateNetwork?: boolean; - groupChannels?: string[]; - dmAllowlist?: string[]; - autoDiscoverChannels?: boolean; - ownerShip?: string; -}; +export type TlonSetupInput = ChannelSetupInput & TlonAccountFieldsInput; function isConfigured(account: TlonResolvedAccount): boolean { return Boolean(account.ship && account.url && account.code); diff --git a/extensions/tlon/src/targets.ts b/extensions/tlon/src/targets.ts index b8aa17e5e8c..4312fd7588f 100644 --- a/extensions/tlon/src/targets.ts +++ b/extensions/tlon/src/targets.ts @@ -23,6 +23,15 @@ export function parseChannelNest(raw: string): { hostShip: string; channelName: return { hostShip, channelName }; } +function makeGroupTarget(parsed: { hostShip: string; channelName: string }): TlonTarget { + return { + kind: "group", + nest: `chat/${parsed.hostShip}/${parsed.channelName}`, + hostShip: parsed.hostShip, + channelName: parsed.channelName, + }; +} + export function parseTlonTarget(raw?: string | null): TlonTarget | null { const trimmed = raw?.trim(); if (!trimmed) { @@ -43,12 +52,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { if (!parsed) { return null; } - return { - kind: "group", - nest: `chat/${parsed.hostShip}/${parsed.channelName}`, - hostShip: parsed.hostShip, - channelName: parsed.channelName, - }; + return makeGroupTarget(parsed); } const parts = groupTarget.split("/"); if (parts.length === 2) { @@ -69,12 +73,7 @@ export function parseTlonTarget(raw?: string | null): TlonTarget | null { if (!parsed) { return null; } - return { - kind: "group", - nest: `chat/${parsed.hostShip}/${parsed.channelName}`, - hostShip: parsed.hostShip, - channelName: parsed.channelName, - }; + return makeGroupTarget(parsed); } if (SHIP_RE.test(withoutPrefix)) { diff --git a/extensions/tlon/src/urbit/base-url.test.ts b/extensions/tlon/src/urbit/base-url.test.ts index 83a9f0a7ac3..117361490ea 100644 --- a/extensions/tlon/src/urbit/base-url.test.ts +++ b/extensions/tlon/src/urbit/base-url.test.ts @@ -2,12 +2,17 @@ import { describe, expect, it } from "vitest"; import { validateUrbitBaseUrl } from "./base-url.js"; describe("validateUrbitBaseUrl", () => { - it("adds https:// when scheme is missing and strips path/query fragments", () => { - const result = validateUrbitBaseUrl("example.com/foo?bar=baz"); + function expectValidBaseUrl(raw: string) { + const result = validateUrbitBaseUrl(raw); expect(result.ok).toBe(true); if (!result.ok) { - return; + throw new Error(result.error); } + return result; + } + + it("adds https:// when scheme is missing and strips path/query fragments", () => { + const result = expectValidBaseUrl("example.com/foo?bar=baz"); expect(result.baseUrl).toBe("https://example.com"); expect(result.hostname).toBe("example.com"); }); @@ -31,21 +36,13 @@ describe("validateUrbitBaseUrl", () => { }); it("normalizes a trailing dot in the hostname for origin construction", () => { - const result = validateUrbitBaseUrl("https://example.com./foo"); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } + const result = expectValidBaseUrl("https://example.com./foo"); expect(result.baseUrl).toBe("https://example.com"); expect(result.hostname).toBe("example.com"); }); it("preserves port in the normalized origin", () => { - const result = validateUrbitBaseUrl("http://example.com:8080/~/login"); - expect(result.ok).toBe(true); - if (!result.ok) { - return; - } + const result = expectValidBaseUrl("http://example.com:8080/~/login"); expect(result.baseUrl).toBe("http://example.com:8080"); }); }); diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 38990099b18..1a22b6ba851 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -78,6 +78,18 @@ export class UrbitSSEClient { this.fetchImpl = options.fetchImpl; } + private channelRequestContext() { + return { + baseUrl: this.url, + cookie: this.cookie, + ship: this.ship, + channelId: this.channelId, + ssrfPolicy: this.ssrfPolicy, + lookupFn: this.lookupFn, + fetchImpl: this.fetchImpl, + }; + } + async subscribe(params: { app: string; path: string; @@ -133,21 +145,10 @@ export class UrbitSSEClient { } async connect() { - await ensureUrbitChannelOpen( - { - baseUrl: this.url, - cookie: this.cookie, - ship: this.ship, - channelId: this.channelId, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - }, - { - createBody: this.subscriptions, - createAuditContext: "tlon-urbit-channel-create", - }, - ); + await ensureUrbitChannelOpen(this.channelRequestContext(), { + createBody: this.subscriptions, + createAuditContext: "tlon-urbit-channel-create", + }); await this.openStream(); this.isConnected = true; @@ -305,18 +306,10 @@ export class UrbitSSEClient { } async poke(params: { app: string; mark: string; json: unknown }) { - return await pokeUrbitChannel( - { - baseUrl: this.url, - cookie: this.cookie, - ship: this.ship, - channelId: this.channelId, - ssrfPolicy: this.ssrfPolicy, - lookupFn: this.lookupFn, - fetchImpl: this.fetchImpl, - }, - { ...params, auditContext: "tlon-urbit-poke" }, - ); + return await pokeUrbitChannel(this.channelRequestContext(), { + ...params, + auditContext: "tlon-urbit-poke", + }); } async scry(path: string) {