fix(zalouser): fix outbound target routing between DM and group

Add explicit target parsing with group:/user: prefixes so the bot
correctly routes outbound messages to groups vs DMs. Supports
aliases (g:, g-, u:, dm:, zlu:, zalouser:) and passes isGroup
flag to sendMessageZalouser.
This commit is contained in:
Tom
2026-03-03 13:48:01 +07:00
parent 98bc429c1a
commit a8a545fc5a
2 changed files with 110 additions and 13 deletions

View File

@@ -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);
});
});

View File

@@ -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<ResolvedZalouserAccount> = {
},
},
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: "<threadId>",
hint: "<threadId|user:id|group:id>",
},
},
directory: {
@@ -596,7 +647,11 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
},
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<ResolvedZalouserAccount> = {
},
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,
});