From b3c78e5e05402781416c01316f910d37e5c01f96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 17:54:24 +0000 Subject: [PATCH] refactor(outbound): reuse signal uuid detection and payload types --- src/infra/outbound/delivery-queue.ts | 22 ++++------ src/infra/outbound/outbound-session.ts | 57 +++++++------------------- src/signal/identity.test.ts | 56 +++++++++++++++++++++++++ src/signal/identity.ts | 2 +- 4 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 src/signal/identity.test.ts diff --git a/src/infra/outbound/delivery-queue.ts b/src/infra/outbound/delivery-queue.ts index d5bba175eee..04f1baddd8a 100644 --- a/src/infra/outbound/delivery-queue.ts +++ b/src/infra/outbound/delivery-queue.ts @@ -25,9 +25,7 @@ type DeliveryMirrorPayload = { mediaUrls?: string[]; }; -export interface QueuedDelivery { - id: string; - enqueuedAt: number; +type QueuedDeliveryPayload = { channel: Exclude; to: string; accountId?: string; @@ -43,6 +41,11 @@ export interface QueuedDelivery { gifPlayback?: boolean; silent?: boolean; mirror?: DeliveryMirrorPayload; +}; + +export interface QueuedDelivery extends QueuedDeliveryPayload { + id: string; + enqueuedAt: number; retryCount: number; lastError?: string; } @@ -65,18 +68,7 @@ export async function ensureQueueDir(stateDir?: string): Promise { } /** Persist a delivery entry to disk before attempting send. Returns the entry ID. */ -type QueuedDeliveryParams = { - channel: Exclude; - to: string; - accountId?: string; - payloads: ReplyPayload[]; - threadId?: string | number | null; - replyToId?: string | null; - bestEffort?: boolean; - gifPlayback?: boolean; - silent?: boolean; - mirror?: DeliveryMirrorPayload; -}; +type QueuedDeliveryParams = QueuedDeliveryPayload; export async function enqueueDelivery( params: QueuedDeliveryParams, diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index 17b9c901a19..7a764fa53c3 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -9,6 +9,7 @@ import { parseIMessageTarget, normalizeIMessageHandle } from "../../imessage/tar import { buildAgentSessionKey, type RoutePeer } from "../../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../../routing/session-key.js"; import { + looksLikeUuid, resolveSignalPeerId, resolveSignalRecipient, resolveSignalSender, @@ -44,22 +45,9 @@ export type ResolveOutboundSessionRouteParams = { threadId?: string | number | null; }; -const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i; // Cache Slack channel type lookups to avoid repeated API calls. const SLACK_CHANNEL_TYPE_CACHE = new Map(); -function looksLikeUuid(value: string): boolean { - if (UUID_RE.test(value) || UUID_COMPACT_RE.test(value)) { - return true; - } - const compact = value.replace(/-/g, ""); - if (!/^[0-9a-f]+$/i.test(compact)) { - return false; - } - return /[a-f]/i.test(compact); -} - function normalizeThreadId(value?: string | number | null): string | undefined { if (value == null) { return undefined; @@ -669,9 +657,15 @@ function resolveNextcloudTalkSession( function resolveZaloSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { - const trimmed = stripProviderPrefix(params.target, "zalo") - .replace(/^(zl):/i, "") - .trim(); + return resolveZaloLikeSession(params, "zalo", /^(zl):/i); +} + +function resolveZaloLikeSession( + params: ResolveOutboundSessionRouteParams, + channel: "zalo" | "zalouser", + aliasPrefix: RegExp, +): OutboundSessionRoute | null { + const trimmed = stripProviderPrefix(params.target, channel).replace(aliasPrefix, "").trim(); if (!trimmed) { return null; } @@ -681,7 +675,7 @@ function resolveZaloSession( const baseSessionKey = buildBaseSessionKey({ cfg: params.cfg, agentId: params.agentId, - channel: "zalo", + channel, accountId: params.accountId, peer, }); @@ -690,39 +684,16 @@ function resolveZaloSession( baseSessionKey, peer, chatType: isGroup ? "group" : "direct", - from: isGroup ? `zalo:group:${peerId}` : `zalo:${peerId}`, - to: `zalo:${peerId}`, + from: isGroup ? `${channel}:group:${peerId}` : `${channel}:${peerId}`, + to: `${channel}:${peerId}`, }; } function resolveZalouserSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { - const trimmed = stripProviderPrefix(params.target, "zalouser") - .replace(/^(zlu):/i, "") - .trim(); - if (!trimmed) { - return null; - } - const isGroup = trimmed.toLowerCase().startsWith("group:"); - const peerId = stripKindPrefix(trimmed); // Keep DM vs group aligned with inbound sessions for Zalo Personal. - const peer: RoutePeer = { kind: isGroup ? "group" : "direct", id: peerId }; - const baseSessionKey = buildBaseSessionKey({ - cfg: params.cfg, - agentId: params.agentId, - channel: "zalouser", - accountId: params.accountId, - peer, - }); - return { - sessionKey: baseSessionKey, - baseSessionKey, - peer, - chatType: isGroup ? "group" : "direct", - from: isGroup ? `zalouser:group:${peerId}` : `zalouser:${peerId}`, - to: `zalouser:${peerId}`, - }; + return resolveZaloLikeSession(params, "zalouser", /^(zlu):/i); } function resolveNostrSession( diff --git a/src/signal/identity.test.ts b/src/signal/identity.test.ts new file mode 100644 index 00000000000..b6f35ab6471 --- /dev/null +++ b/src/signal/identity.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { + looksLikeUuid, + resolveSignalPeerId, + resolveSignalRecipient, + resolveSignalSender, +} from "./identity.js"; + +describe("looksLikeUuid", () => { + it("accepts hyphenated UUIDs", () => { + expect(looksLikeUuid("123e4567-e89b-12d3-a456-426614174000")).toBe(true); + }); + + it("accepts compact UUIDs", () => { + expect(looksLikeUuid("123e4567e89b12d3a456426614174000")).toBe(true); + }); + + it("accepts uuid-like hex values with letters", () => { + expect(looksLikeUuid("abcd-1234")).toBe(true); + }); + + it("rejects numeric ids and phone-like values", () => { + expect(looksLikeUuid("1234567890")).toBe(false); + expect(looksLikeUuid("+15555551212")).toBe(false); + }); +}); + +describe("signal sender identity", () => { + it("prefers sourceNumber over sourceUuid", () => { + const sender = resolveSignalSender({ + sourceNumber: " +15550001111 ", + sourceUuid: "123e4567-e89b-12d3-a456-426614174000", + }); + expect(sender).toEqual({ + kind: "phone", + raw: "+15550001111", + e164: "+15550001111", + }); + }); + + it("uses sourceUuid when sourceNumber is missing", () => { + const sender = resolveSignalSender({ + sourceUuid: "123e4567-e89b-12d3-a456-426614174000", + }); + expect(sender).toEqual({ + kind: "uuid", + raw: "123e4567-e89b-12d3-a456-426614174000", + }); + }); + + it("maps uuid senders to recipient and peer ids", () => { + const sender = { kind: "uuid", raw: "123e4567-e89b-12d3-a456-426614174000" } as const; + expect(resolveSignalRecipient(sender)).toBe("123e4567-e89b-12d3-a456-426614174000"); + expect(resolveSignalPeerId(sender)).toBe("uuid:123e4567-e89b-12d3-a456-426614174000"); + }); +}); diff --git a/src/signal/identity.ts b/src/signal/identity.ts index 95d27e042ce..ca8f9812644 100644 --- a/src/signal/identity.ts +++ b/src/signal/identity.ts @@ -12,7 +12,7 @@ type SignalAllowEntry = const UUID_HYPHENATED_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i; -function looksLikeUuid(value: string): boolean { +export function looksLikeUuid(value: string): boolean { if (UUID_HYPHENATED_RE.test(value) || UUID_COMPACT_RE.test(value)) { return true; }