diff --git a/extensions/zalouser/src/channel.sendpayload.test.ts b/extensions/zalouser/src/channel.sendpayload.test.ts index cdf478411f0..00910646325 100644 --- a/extensions/zalouser/src/channel.sendpayload.test.ts +++ b/extensions/zalouser/src/channel.sendpayload.test.ts @@ -49,6 +49,22 @@ describe("zalouserPlugin outbound sendPayload", () => { expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-t1" }); }); + it("group target delegates with isGroup=true and stripped threadId", async () => { + mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-g1" }); + + const result = await zalouserPlugin.outbound!.sendPayload!({ + ...baseCtx({ text: "hello group" }), + to: "group:1471383327500481391", + }); + + expect(mockedSend).toHaveBeenCalledWith( + "1471383327500481391", + "hello group", + expect.objectContaining({ isGroup: true }), + ); + expect(result).toMatchObject({ channel: "zalouser", messageId: "zlu-g1" }); + }); + it("single media delegates to sendMedia", async () => { mockedSend.mockResolvedValue({ ok: true, messageId: "zlu-m1" }); @@ -115,3 +131,27 @@ describe("zalouserPlugin outbound sendPayload", () => { expect(result).toMatchObject({ channel: "zalouser" }); }); }); + +describe("zalouserPlugin messaging target normalization", () => { + it("normalizes user/group aliases to canonical targets", () => { + const normalize = zalouserPlugin.messaging?.normalizeTarget; + expect(normalize).toBeTypeOf("function"); + if (!normalize) { + return; + } + expect(normalize("zlu:g-30003")).toBe("group:30003"); + expect(normalize("zalouser:u:20002")).toBe("user:20002"); + expect(normalize("20002")).toBe("20002"); + }); + + it("treats canonical user/group targets as direct IDs", () => { + const looksLikeId = zalouserPlugin.messaging?.targetResolver?.looksLikeId; + expect(looksLikeId).toBeTypeOf("function"); + if (!looksLikeId) { + return; + } + expect(looksLikeId("user:20002")).toBe(true); + expect(looksLikeId("group:30003")).toBe(true); + expect(looksLikeId("Alice Nguyen")).toBe(false); + }); +}); diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 2c1770b6ebd..ad34fd49722 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -61,6 +61,60 @@ const meta = { quickstartAllowFrom: true, }; +function stripZalouserTargetPrefix(raw: string): string { + return raw + .trim() + .replace(/^(zalouser|zlu):/i, "") + .trim(); +} + +function normalizePrefixedTarget(raw: string): string | undefined { + const trimmed = stripZalouserTargetPrefix(raw); + if (!trimmed) { + return undefined; + } + + const groupAlias = trimmed.match(/^(group:|g:|g-)(.+)$/i); + if (groupAlias) { + const id = groupAlias[2]?.trim() ?? ""; + return id ? `group:${id}` : undefined; + } + + const userAlias = trimmed.match(/^(user:|dm:|u:|u-)(.+)$/i); + if (userAlias) { + const id = userAlias[2]?.trim() ?? ""; + return id ? `user:${id}` : undefined; + } + + return trimmed; +} + +function parseZalouserOutboundTarget(raw: string): { + threadId: string; + isGroup: boolean; +} { + const normalized = normalizePrefixedTarget(raw); + if (!normalized) { + throw new Error("Zalouser target is required"); + } + const lowered = normalized.toLowerCase(); + if (lowered.startsWith("group:")) { + const threadId = normalized.slice("group:".length).trim(); + if (!threadId) { + throw new Error("Zalouser group target is missing group id"); + } + return { threadId, isGroup: true }; + } + if (lowered.startsWith("user:")) { + const threadId = normalized.slice("user:".length).trim(); + if (!threadId) { + throw new Error("Zalouser user target is missing user id"); + } + return { threadId, isGroup: false }; + } + return { threadId: normalized, isGroup: false }; +} + function resolveZalouserQrProfile(accountId?: string | null): string { const normalized = normalizeAccountId(accountId); if (!normalized || normalized === DEFAULT_ACCOUNT_ID) { @@ -387,22 +441,19 @@ export const zalouserPlugin: ChannelPlugin = { }, }, messaging: { - normalizeTarget: (raw) => { - const trimmed = raw?.trim(); - if (!trimmed) { - return undefined; - } - return trimmed.replace(/^(zalouser|zlu):/i, ""); - }, + normalizeTarget: (raw) => normalizePrefixedTarget(raw), targetResolver: { looksLikeId: (raw) => { - const trimmed = raw.trim(); - if (!trimmed) { + const normalized = normalizePrefixedTarget(raw); + if (!normalized) { return false; } - return /^\d{3,}$/.test(trimmed); + if (/^group:[^\s]+$/i.test(normalized) || /^user:[^\s]+$/i.test(normalized)) { + return true; + } + return /^\d{3,}$/.test(normalized); }, - hint: "", + hint: "", }, }, directory: { @@ -596,7 +647,11 @@ export const zalouserPlugin: ChannelPlugin = { }, sendText: async ({ to, text, accountId, cfg }) => { const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); - const result = await sendMessageZalouser(to, text, { profile: account.profile }); + const target = parseZalouserOutboundTarget(to); + const result = await sendMessageZalouser(target.threadId, text, { + profile: account.profile, + isGroup: target.isGroup, + }); return { channel: "zalouser", ok: result.ok, @@ -606,8 +661,10 @@ export const zalouserPlugin: ChannelPlugin = { }, sendMedia: async ({ to, text, mediaUrl, accountId, cfg, mediaLocalRoots }) => { const account = resolveZalouserAccountSync({ cfg: cfg, accountId }); - const result = await sendMessageZalouser(to, text, { + const target = parseZalouserOutboundTarget(to); + const result = await sendMessageZalouser(target.threadId, text, { profile: account.profile, + isGroup: target.isGroup, mediaUrl, mediaLocalRoots, });