refactor: prune legacy group prefixes
This commit is contained in:
@@ -534,7 +534,7 @@ async function processMessageWithPipeline(params: {
|
|||||||
Body: body,
|
Body: body,
|
||||||
RawBody: rawBody,
|
RawBody: rawBody,
|
||||||
CommandBody: rawBody,
|
CommandBody: rawBody,
|
||||||
From: isGroup ? `group:${chatId}` : `zalo:${senderId}`,
|
From: isGroup ? `zalo:group:${chatId}` : `zalo:${senderId}`,
|
||||||
To: `zalo:${chatId}`,
|
To: `zalo:${chatId}`,
|
||||||
SessionKey: route.sessionKey,
|
SessionKey: route.sessionKey,
|
||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
|
|||||||
@@ -195,9 +195,8 @@ async function processMessage(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const fromLabel = isGroup
|
const rawBody = content.trim();
|
||||||
? `group:${chatId}`
|
const fromLabel = isGroup ? `group:${chatId}` : senderName || `user:${senderId}`;
|
||||||
: senderName || `user:${senderId}`;
|
|
||||||
const body = deps.formatAgentEnvelope({
|
const body = deps.formatAgentEnvelope({
|
||||||
channel: "Zalo Personal",
|
channel: "Zalo Personal",
|
||||||
from: fromLabel,
|
from: fromLabel,
|
||||||
@@ -209,7 +208,7 @@ async function processMessage(
|
|||||||
Body: body,
|
Body: body,
|
||||||
RawBody: rawBody,
|
RawBody: rawBody,
|
||||||
CommandBody: rawBody,
|
CommandBody: rawBody,
|
||||||
From: isGroup ? `group:${chatId}` : `zalouser:${senderId}`,
|
From: isGroup ? `zalouser:group:${chatId}` : `zalouser:${senderId}`,
|
||||||
To: `zalouser:${chatId}`,
|
To: `zalouser:${chatId}`,
|
||||||
SessionKey: route.sessionKey,
|
SessionKey: route.sessionKey,
|
||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
|
|||||||
@@ -309,7 +309,6 @@ export function createSessionStatusTool(opts?: {
|
|||||||
const isGroup =
|
const isGroup =
|
||||||
resolved.entry.chatType === "group" ||
|
resolved.entry.chatType === "group" ||
|
||||||
resolved.entry.chatType === "channel" ||
|
resolved.entry.chatType === "channel" ||
|
||||||
resolved.key.startsWith("group:") ||
|
|
||||||
resolved.key.includes(":group:") ||
|
resolved.key.includes(":group:") ||
|
||||||
resolved.key.includes(":channel:");
|
resolved.key.includes(":channel:");
|
||||||
const groupActivation = isGroup
|
const groupActivation = isGroup
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function classifySessionKind(params: {
|
|||||||
if (key.startsWith("hook:")) return "hook";
|
if (key.startsWith("hook:")) return "hook";
|
||||||
if (key.startsWith("node-") || key.startsWith("node:")) return "node";
|
if (key.startsWith("node-") || key.startsWith("node:")) return "node";
|
||||||
if (params.gatewayKind === "group") return "group";
|
if (params.gatewayKind === "group") return "group";
|
||||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
return "other";
|
return "other";
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ export function resolveAnnounceTargetFromKey(sessionKey: string): AnnounceTarget
|
|||||||
if (!channelRaw) return null;
|
if (!channelRaw) return null;
|
||||||
const normalizedChannel = normalizeChannelId(channelRaw);
|
const normalizedChannel = normalizeChannelId(channelRaw);
|
||||||
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
const channel = normalizedChannel ?? channelRaw.toLowerCase();
|
||||||
const kindTarget = normalizedChannel
|
const kindTarget = (() => {
|
||||||
? kind === "channel"
|
if (!normalizedChannel) return id;
|
||||||
? `channel:${id}`
|
if (normalizedChannel === "discord" || normalizedChannel === "slack") {
|
||||||
: `group:${id}`
|
return `channel:${id}`;
|
||||||
: id;
|
}
|
||||||
|
return kind === "channel" ? `channel:${id}` : `group:${id}`;
|
||||||
|
})();
|
||||||
const normalized = normalizedChannel
|
const normalized = normalizedChannel
|
||||||
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(kindTarget)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ describe("group intro prompts", () => {
|
|||||||
await getReplyFromConfig(
|
await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "status update",
|
Body: "status update",
|
||||||
From: "group:dev",
|
From: "discord:group:dev",
|
||||||
To: "+1888",
|
To: "+1888",
|
||||||
ChatType: "group",
|
ChatType: "group",
|
||||||
GroupSubject: "Release Squad",
|
GroupSubject: "Release Squad",
|
||||||
@@ -172,7 +172,7 @@ describe("group intro prompts", () => {
|
|||||||
await getReplyFromConfig(
|
await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "ping",
|
Body: "ping",
|
||||||
From: "group:tg",
|
From: "telegram:group:tg",
|
||||||
To: "+1777",
|
To: "+1777",
|
||||||
ChatType: "group",
|
ChatType: "group",
|
||||||
GroupSubject: "Dev Chat",
|
GroupSubject: "Dev Chat",
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ describe("trigger handling", () => {
|
|||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/elevated on",
|
Body: "/elevated on",
|
||||||
From: "group:123@g.us",
|
From: "whatsapp:group:123@g.us",
|
||||||
To: "whatsapp:+2000",
|
To: "whatsapp:+2000",
|
||||||
Provider: "whatsapp",
|
Provider: "whatsapp",
|
||||||
SenderE164: "+1000",
|
SenderE164: "+1000",
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ describe("trigger handling", () => {
|
|||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/elevated off",
|
Body: "/elevated off",
|
||||||
From: "group:123@g.us",
|
From: "whatsapp:group:123@g.us",
|
||||||
To: "whatsapp:+2000",
|
To: "whatsapp:+2000",
|
||||||
Provider: "whatsapp",
|
Provider: "whatsapp",
|
||||||
SenderE164: "+1000",
|
SenderE164: "+1000",
|
||||||
@@ -172,7 +172,7 @@ describe("trigger handling", () => {
|
|||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/elevated on",
|
Body: "/elevated on",
|
||||||
From: "group:123@g.us",
|
From: "whatsapp:group:123@g.us",
|
||||||
To: "whatsapp:+2000",
|
To: "whatsapp:+2000",
|
||||||
Provider: "whatsapp",
|
Provider: "whatsapp",
|
||||||
SenderE164: "+1000",
|
SenderE164: "+1000",
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ describe("trigger handling", () => {
|
|||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
Body: "hi",
|
Body: "hi",
|
||||||
From: "group:whatsapp:demo",
|
From: "whatsapp:group:demo",
|
||||||
To: "+2000",
|
To: "+2000",
|
||||||
ChatType: "group" as const,
|
ChatType: "group" as const,
|
||||||
Provider: "whatsapp" as const,
|
Provider: "whatsapp" as const,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe("buildThreadingToolContext", () => {
|
|||||||
const sessionCtx = {
|
const sessionCtx = {
|
||||||
Provider: "imessage",
|
Provider: "imessage",
|
||||||
ChatType: "group",
|
ChatType: "group",
|
||||||
From: "group:7",
|
From: "imessage:group:7",
|
||||||
To: "chat_id:7",
|
To: "chat_id:7",
|
||||||
} as TemplateContext;
|
} as TemplateContext;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("resolveGroupRequireMention", () => {
|
|||||||
};
|
};
|
||||||
const ctx: TemplateContext = {
|
const ctx: TemplateContext = {
|
||||||
Provider: "discord",
|
Provider: "discord",
|
||||||
From: "group:123",
|
From: "discord:group:123",
|
||||||
GroupChannel: "#general",
|
GroupChannel: "#general",
|
||||||
GroupSpace: "145",
|
GroupSpace: "145",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,26 @@ import { isInternalMessageChannel } from "../../utils/message-channel.js";
|
|||||||
import { normalizeGroupActivation } from "../group-activation.js";
|
import { normalizeGroupActivation } from "../group-activation.js";
|
||||||
import type { TemplateContext } from "../templating.js";
|
import type { TemplateContext } from "../templating.js";
|
||||||
|
|
||||||
|
function extractGroupId(raw: string | undefined | null): string | undefined {
|
||||||
|
const trimmed = (raw ?? "").trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
const parts = trimmed.split(":").filter(Boolean);
|
||||||
|
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
|
||||||
|
return parts.slice(2).join(":") || undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parts.length >= 2 &&
|
||||||
|
parts[0]?.toLowerCase() === "whatsapp" &&
|
||||||
|
trimmed.toLowerCase().includes("@g.us")
|
||||||
|
) {
|
||||||
|
return parts.slice(1).join(":") || undefined;
|
||||||
|
}
|
||||||
|
if (parts.length >= 2 && (parts[0] === "group" || parts[0] === "channel")) {
|
||||||
|
return parts.slice(1).join(":") || undefined;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveGroupRequireMention(params: {
|
export function resolveGroupRequireMention(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
ctx: TemplateContext;
|
ctx: TemplateContext;
|
||||||
@@ -15,7 +35,7 @@ export function resolveGroupRequireMention(params: {
|
|||||||
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
|
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
|
||||||
const channel = normalizeChannelId(rawChannel);
|
const channel = normalizeChannelId(rawChannel);
|
||||||
if (!channel) return true;
|
if (!channel) return true;
|
||||||
const groupId = groupResolution?.id ?? ctx.From?.replace(/^group:/, "");
|
const groupId = groupResolution?.id ?? extractGroupId(ctx.From);
|
||||||
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
|
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
|
||||||
const groupSpace = ctx.GroupSpace?.trim();
|
const groupSpace = ctx.GroupSpace?.trim();
|
||||||
const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
|
const requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
|
||||||
@@ -61,7 +81,7 @@ export function buildGroupIntro(params: {
|
|||||||
activation === "always"
|
activation === "always"
|
||||||
? "Activation: always-on (you receive every group message)."
|
? "Activation: always-on (you receive every group message)."
|
||||||
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
|
: "Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included).";
|
||||||
const groupId = params.sessionCtx.From?.replace(/^group:/, "");
|
const groupId = params.sessionEntry?.groupId ?? extractGroupId(params.sessionCtx.From);
|
||||||
const groupChannel = params.sessionCtx.GroupChannel?.trim() ?? subject;
|
const groupChannel = params.sessionCtx.GroupChannel?.trim() ?? subject;
|
||||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||||
const providerIdsLine = providerId
|
const providerIdsLine = providerId
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("finalizeInboundContext", () => {
|
|||||||
Body: "a\\nb\r\nc",
|
Body: "a\\nb\r\nc",
|
||||||
RawBody: "raw\\nline",
|
RawBody: "raw\\nline",
|
||||||
ChatType: "channel",
|
ChatType: "channel",
|
||||||
From: "group:123@g.us",
|
From: "whatsapp:group:123@g.us",
|
||||||
GroupSubject: "Test",
|
GroupSubject: "Test",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export async function initSessionState(params: {
|
|||||||
displayName: baseEntry?.displayName,
|
displayName: baseEntry?.displayName,
|
||||||
chatType: baseEntry?.chatType,
|
chatType: baseEntry?.chatType,
|
||||||
channel: baseEntry?.channel,
|
channel: baseEntry?.channel,
|
||||||
|
groupId: baseEntry?.groupId,
|
||||||
subject: baseEntry?.subject,
|
subject: baseEntry?.subject,
|
||||||
room: baseEntry?.room,
|
room: baseEntry?.room,
|
||||||
space: baseEntry?.space,
|
space: baseEntry?.space,
|
||||||
@@ -256,6 +257,7 @@ export async function initSessionState(params: {
|
|||||||
const nextSubject = nextRoom ? undefined : subject;
|
const nextSubject = nextRoom ? undefined : subject;
|
||||||
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
sessionEntry.chatType = groupResolution.chatType ?? "group";
|
||||||
sessionEntry.channel = channel;
|
sessionEntry.channel = channel;
|
||||||
|
sessionEntry.groupId = groupResolution.id;
|
||||||
if (nextSubject) sessionEntry.subject = nextSubject;
|
if (nextSubject) sessionEntry.subject = nextSubject;
|
||||||
if (nextRoom) sessionEntry.room = nextRoom;
|
if (nextRoom) sessionEntry.room = nextRoom;
|
||||||
if (space) sessionEntry.space = space;
|
if (space) sessionEntry.space = space;
|
||||||
|
|||||||
@@ -293,8 +293,7 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
entry?.chatType === "group" ||
|
entry?.chatType === "group" ||
|
||||||
entry?.chatType === "channel" ||
|
entry?.chatType === "channel" ||
|
||||||
Boolean(args.sessionKey?.includes(":group:")) ||
|
Boolean(args.sessionKey?.includes(":group:")) ||
|
||||||
Boolean(args.sessionKey?.includes(":channel:")) ||
|
Boolean(args.sessionKey?.includes(":channel:"));
|
||||||
Boolean(args.sessionKey?.startsWith("group:"));
|
|
||||||
const groupActivationValue = isGroupSession
|
const groupActivationValue = isGroupSession
|
||||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ export function normalizeSlackMessagingTarget(raw: string): string | undefined {
|
|||||||
const id = trimmed.slice(8).trim();
|
const id = trimmed.slice(8).trim();
|
||||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("group:")) {
|
|
||||||
const id = trimmed.slice(6).trim();
|
|
||||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
|
||||||
}
|
|
||||||
if (trimmed.startsWith("slack:")) {
|
if (trimmed.startsWith("slack:")) {
|
||||||
const id = trimmed.slice(6).trim();
|
const id = trimmed.slice(6).trim();
|
||||||
return id ? `user:${id}`.toLowerCase() : undefined;
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
@@ -36,7 +32,7 @@ export function looksLikeSlackTargetId(raw: string): boolean {
|
|||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return false;
|
if (!trimmed) return false;
|
||||||
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) return true;
|
if (/^<@([A-Z0-9]+)>$/i.test(trimmed)) return true;
|
||||||
if (/^(user|channel|group):/i.test(trimmed)) return true;
|
if (/^(user|channel):/i.test(trimmed)) return true;
|
||||||
if (/^slack:/i.test(trimmed)) return true;
|
if (/^slack:/i.test(trimmed)) return true;
|
||||||
if (/^[@#]/.test(trimmed)) return true;
|
if (/^[@#]/.test(trimmed)) return true;
|
||||||
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
|
return /^[CUWGD][A-Z0-9]{8,}$/i.test(trimmed);
|
||||||
@@ -55,10 +51,6 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined
|
|||||||
const id = trimmed.slice(8).trim();
|
const id = trimmed.slice(8).trim();
|
||||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
return id ? `channel:${id}`.toLowerCase() : undefined;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("group:")) {
|
|
||||||
const id = trimmed.slice(6).trim();
|
|
||||||
return id ? `channel:${id}`.toLowerCase() : undefined;
|
|
||||||
}
|
|
||||||
if (trimmed.startsWith("discord:")) {
|
if (trimmed.startsWith("discord:")) {
|
||||||
const id = trimmed.slice(8).trim();
|
const id = trimmed.slice(8).trim();
|
||||||
return id ? `user:${id}`.toLowerCase() : undefined;
|
return id ? `user:${id}`.toLowerCase() : undefined;
|
||||||
@@ -74,7 +66,7 @@ export function looksLikeDiscordTargetId(raw: string): boolean {
|
|||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return false;
|
if (!trimmed) return false;
|
||||||
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
if (/^<@!?\d+>$/.test(trimmed)) return true;
|
||||||
if (/^(user|channel|group|discord):/i.test(trimmed)) return true;
|
if (/^(user|channel|discord):/i.test(trimmed)) return true;
|
||||||
if (/^\d{6,}$/.test(trimmed)) return true;
|
if (/^\d{6,}$/.test(trimmed)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -87,8 +79,6 @@ export function normalizeTelegramMessagingTarget(raw: string): string | undefine
|
|||||||
normalized = normalized.slice("telegram:".length).trim();
|
normalized = normalized.slice("telegram:".length).trim();
|
||||||
} else if (normalized.startsWith("tg:")) {
|
} else if (normalized.startsWith("tg:")) {
|
||||||
normalized = normalized.slice("tg:".length).trim();
|
normalized = normalized.slice("tg:".length).trim();
|
||||||
} else if (normalized.startsWith("group:")) {
|
|
||||||
normalized = normalized.slice("group:".length).trim();
|
|
||||||
}
|
}
|
||||||
if (!normalized) return undefined;
|
if (!normalized) return undefined;
|
||||||
const tmeMatch =
|
const tmeMatch =
|
||||||
@@ -102,7 +92,7 @@ export function normalizeTelegramMessagingTarget(raw: string): string | undefine
|
|||||||
export function looksLikeTelegramTargetId(raw: string): boolean {
|
export function looksLikeTelegramTargetId(raw: string): boolean {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return false;
|
if (!trimmed) return false;
|
||||||
if (/^(telegram|tg|group):/i.test(trimmed)) return true;
|
if (/^(telegram|tg):/i.test(trimmed)) return true;
|
||||||
if (trimmed.startsWith("@")) return true;
|
if (trimmed.startsWith("@")) return true;
|
||||||
return /^-?\d{6,}$/.test(trimmed);
|
return /^-?\d{6,}$/.test(trimmed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ function getSessionRecipients(cfg: ClawdbotConfig) {
|
|||||||
const storePath = resolveStorePath(cfg.session?.store);
|
const storePath = resolveStorePath(cfg.session?.store);
|
||||||
const store = loadSessionStore(storePath);
|
const store = loadSessionStore(storePath);
|
||||||
const isGroupKey = (key: string) =>
|
const isGroupKey = (key: string) =>
|
||||||
key.startsWith("group:") ||
|
key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
|
||||||
key.includes(":group:") ||
|
|
||||||
key.includes(":channel:") ||
|
|
||||||
key.includes("@g.us");
|
|
||||||
const isCronKey = (key: string) => key.startsWith("cron:");
|
const isCronKey = (key: string) => key.startsWith("cron:");
|
||||||
|
|
||||||
const recipients = Object.entries(store)
|
const recipients = Object.entries(store)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe("doctor legacy state migrations", () => {
|
|||||||
) as Record<string, { sessionId: string }>;
|
) as Record<string, { sessionId: string }>;
|
||||||
expect(store["agent:main:main"]?.sessionId).toBe("b");
|
expect(store["agent:main:main"]?.sessionId).toBe("b");
|
||||||
expect(store["agent:main:slack:channel:C123"]?.sessionId).toBe("c");
|
expect(store["agent:main:slack:channel:C123"]?.sessionId).toBe("c");
|
||||||
expect(store["group:abc"]?.sessionId).toBe("d");
|
expect(store["agent:main:unknown:group:abc"]?.sessionId).toBe("d");
|
||||||
expect(store["agent:main:subagent:xyz"]?.sessionId).toBe("e");
|
expect(store["agent:main:subagent:xyz"]?.sessionId).toBe("e");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ function classifyKey(key: string, entry?: SessionEntry): SessionRow["kind"] {
|
|||||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
return "direct";
|
return "direct";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const classifyKey = (key: string, entry?: SessionEntry): SessionStatus["kind"] =
|
|||||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
return "direct";
|
return "direct";
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ describe("sessions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps group chats distinct", () => {
|
it("keeps group chats distinct", () => {
|
||||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe("group:12345-678@g.us");
|
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe(
|
||||||
|
"whatsapp:group:12345-678@g.us",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefixes group keys with provider when available", () => {
|
it("prefixes group keys with provider when available", () => {
|
||||||
@@ -45,7 +47,7 @@ describe("sessions", () => {
|
|||||||
|
|
||||||
it("keeps explicit provider when provided in group key", () => {
|
it("keeps explicit provider when provided in group key", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveSessionKey("per-sender", { From: "group:discord:12345", ChatType: "group" }, "main"),
|
resolveSessionKey("per-sender", { From: "discord:group:12345", ChatType: "group" }, "main"),
|
||||||
).toBe("agent:main:discord:group:12345");
|
).toBe("agent:main:discord:group:12345");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ describe("sessions", () => {
|
|||||||
|
|
||||||
it("leaves groups untouched even with main key", () => {
|
it("leaves groups untouched even with main key", () => {
|
||||||
expect(resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main")).toBe(
|
expect(resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main")).toBe(
|
||||||
"agent:main:group:12345-678@g.us",
|
"agent:main:whatsapp:group:12345-678@g.us",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function buildGroupDisplayName(params: {
|
|||||||
(room && space
|
(room && space
|
||||||
? `${space}${room.startsWith("#") ? "" : "#"}${room}`
|
? `${space}${room.startsWith("#") ? "" : "#"}${room}`
|
||||||
: room || subject || space || "") || "";
|
: room || subject || space || "") || "";
|
||||||
const fallbackId = params.id?.trim() || params.key.replace(/^group:/, "");
|
const fallbackId = params.id?.trim() || params.key;
|
||||||
const rawLabel = detail || fallbackId;
|
const rawLabel = detail || fallbackId;
|
||||||
let token = normalizeGroupLabel(rawLabel);
|
let token = normalizeGroupLabel(rawLabel);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -52,84 +52,49 @@ export function buildGroupDisplayName(params: {
|
|||||||
|
|
||||||
export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||||
const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
|
const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
|
||||||
if (!from) return null;
|
|
||||||
const chatType = ctx.ChatType?.trim().toLowerCase();
|
const chatType = ctx.ChatType?.trim().toLowerCase();
|
||||||
const isGroup =
|
const normalizedChatType =
|
||||||
chatType === "group" ||
|
chatType === "channel" ? "channel" : chatType === "group" ? "group" : undefined;
|
||||||
from.startsWith("group:") ||
|
|
||||||
from.includes("@g.us") ||
|
const isWhatsAppGroupId = from.toLowerCase().endsWith("@g.us");
|
||||||
|
const looksLikeGroup =
|
||||||
|
normalizedChatType === "group" ||
|
||||||
|
normalizedChatType === "channel" ||
|
||||||
from.includes(":group:") ||
|
from.includes(":group:") ||
|
||||||
from.includes(":channel:");
|
from.includes(":channel:") ||
|
||||||
if (!isGroup) return null;
|
isWhatsAppGroupId;
|
||||||
|
if (!looksLikeGroup) return null;
|
||||||
|
|
||||||
const providerHint = ctx.Provider?.trim().toLowerCase();
|
const providerHint = ctx.Provider?.trim().toLowerCase();
|
||||||
const hasGroupPrefix = from.startsWith("group:");
|
|
||||||
const raw = (hasGroupPrefix ? from.slice("group:".length) : from).trim();
|
|
||||||
|
|
||||||
let provider: string | undefined;
|
const parts = from.split(":").filter(Boolean);
|
||||||
let kind: "group" | "channel" | undefined;
|
const head = parts[0]?.trim().toLowerCase() ?? "";
|
||||||
let id = "";
|
const headIsSurface = head ? getGroupSurfaces().has(head) : false;
|
||||||
|
|
||||||
const parseKind = (value: string) => {
|
const provider = headIsSurface
|
||||||
if (value === "channel") return "channel";
|
? head
|
||||||
return "group";
|
: (providerHint ?? (isWhatsAppGroupId ? "whatsapp" : undefined));
|
||||||
};
|
if (!provider) return null;
|
||||||
|
|
||||||
const parseParts = (parts: string[]) => {
|
const second = parts[1]?.trim().toLowerCase();
|
||||||
if (parts.length >= 2 && getGroupSurfaces().has(parts[0])) {
|
const secondIsKind = second === "group" || second === "channel";
|
||||||
provider = parts[0];
|
const kind = secondIsKind
|
||||||
if (parts.length >= 3) {
|
? (second as "group" | "channel")
|
||||||
const kindCandidate = parts[1];
|
: from.includes(":channel:") || normalizedChatType === "channel"
|
||||||
if (["group", "channel"].includes(kindCandidate)) {
|
? "channel"
|
||||||
kind = parseKind(kindCandidate);
|
: "group";
|
||||||
id = parts.slice(2).join(":");
|
const id = headIsSurface
|
||||||
} else {
|
? secondIsKind
|
||||||
id = parts.slice(1).join(":");
|
? parts.slice(2).join(":")
|
||||||
}
|
: parts.slice(1).join(":")
|
||||||
} else {
|
: from;
|
||||||
id = parts[1];
|
const finalId = id.trim();
|
||||||
}
|
if (!finalId) return null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parts.length >= 2 && ["group", "channel"].includes(parts[0])) {
|
|
||||||
kind = parseKind(parts[0]);
|
|
||||||
id = parts.slice(1).join(":");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hasGroupPrefix) {
|
|
||||||
const legacyParts = raw.split(":").filter(Boolean);
|
|
||||||
if (legacyParts.length > 1) {
|
|
||||||
parseParts(legacyParts);
|
|
||||||
} else {
|
|
||||||
id = raw;
|
|
||||||
}
|
|
||||||
} else if (from.includes("@g.us") && !from.includes(":")) {
|
|
||||||
id = from;
|
|
||||||
} else {
|
|
||||||
parseParts(from.split(":").filter(Boolean));
|
|
||||||
if (!id) {
|
|
||||||
id = raw || from;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedProvider = provider ?? providerHint;
|
|
||||||
if (!resolvedProvider) {
|
|
||||||
const legacy = hasGroupPrefix ? `group:${raw}` : `group:${from}`;
|
|
||||||
return {
|
|
||||||
key: legacy,
|
|
||||||
id: raw || from,
|
|
||||||
chatType: "group",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedKind = kind === "channel" ? "channel" : "group";
|
|
||||||
const key = `${resolvedProvider}:${resolvedKind}:${id || raw || from}`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key: `${provider}:${kind}:${finalId}`,
|
||||||
channel: resolvedProvider,
|
channel: provider,
|
||||||
id: id || raw || from,
|
id: finalId,
|
||||||
chatType: resolvedKind === "channel" ? "channel" : "group",
|
chatType: kind === "channel" ? "channel" : "group",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function resolveSessionKey(scope: SessionScope, ctx: MsgContext, mainKey?
|
|||||||
agentId: DEFAULT_AGENT_ID,
|
agentId: DEFAULT_AGENT_ID,
|
||||||
mainKey: canonicalMainKey,
|
mainKey: canonicalMainKey,
|
||||||
});
|
});
|
||||||
const isGroup = raw.startsWith("group:") || raw.includes(":group:") || raw.includes(":channel:");
|
const isGroup = raw.includes(":group:") || raw.includes(":channel:");
|
||||||
if (!isGroup) return canonical;
|
if (!isGroup) return canonical;
|
||||||
return `agent:${DEFAULT_AGENT_ID}:${raw}`;
|
return `agent:${DEFAULT_AGENT_ID}:${raw}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export type SessionEntry = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
groupId?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
space?: string;
|
space?: string;
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
|||||||
|
|
||||||
const effectiveFrom = isDirectMessage
|
const effectiveFrom = isDirectMessage
|
||||||
? `discord:${author.id}`
|
? `discord:${author.id}`
|
||||||
: (autoThreadContext?.From ?? `group:${message.channelId}`);
|
: (autoThreadContext?.From ?? `discord:channel:${message.channelId}`);
|
||||||
const effectiveTo = autoThreadContext?.To ?? replyTarget;
|
const effectiveTo = autoThreadContext?.To ?? replyTarget;
|
||||||
if (!effectiveTo) {
|
if (!effectiveTo) {
|
||||||
runtime.error?.(danger("discord: missing reply target"));
|
runtime.error?.(danger("discord: missing reply target"));
|
||||||
|
|||||||
@@ -601,7 +601,11 @@ async function dispatchDiscordCommandInteraction(params: {
|
|||||||
RawBody: prompt,
|
RawBody: prompt,
|
||||||
CommandBody: prompt,
|
CommandBody: prompt,
|
||||||
CommandArgs: commandArgs,
|
CommandArgs: commandArgs,
|
||||||
From: isDirectMessage ? `discord:${user.id}` : `group:${channelId}`,
|
From: isDirectMessage
|
||||||
|
? `discord:${user.id}`
|
||||||
|
: isGroupDm
|
||||||
|
? `discord:group:${channelId}`
|
||||||
|
: `discord:channel:${channelId}`,
|
||||||
To: `slash:${user.id}`,
|
To: `slash:${user.id}`,
|
||||||
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
|
SessionKey: `agent:${route.agentId}:${sessionPrefix}:${user.id}`,
|
||||||
CommandTargetSessionKey: route.sessionKey,
|
CommandTargetSessionKey: route.sessionKey,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ describe("resolveDiscordAutoThreadContext", () => {
|
|||||||
});
|
});
|
||||||
expect(context).not.toBeNull();
|
expect(context).not.toBeNull();
|
||||||
expect(context?.To).toBe("channel:thread");
|
expect(context?.To).toBe("channel:thread");
|
||||||
expect(context?.From).toBe("group:thread");
|
expect(context?.From).toBe("discord:channel:thread");
|
||||||
expect(context?.OriginatingTo).toBe("channel:thread");
|
expect(context?.OriginatingTo).toBe("channel:thread");
|
||||||
expect(context?.SessionKey).toBe(
|
expect(context?.SessionKey).toBe(
|
||||||
buildAgentSessionKey({
|
buildAgentSessionKey({
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function resolveDiscordAutoThreadContext(params: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
createdThreadId,
|
createdThreadId,
|
||||||
From: `group:${createdThreadId}`,
|
From: `${params.channel}:channel:${createdThreadId}`,
|
||||||
To: `channel:${createdThreadId}`,
|
To: `channel:${createdThreadId}`,
|
||||||
OriginatingTo: `channel:${createdThreadId}`,
|
OriginatingTo: `channel:${createdThreadId}`,
|
||||||
SessionKey: threadSessionKey,
|
SessionKey: threadSessionKey,
|
||||||
|
|||||||
@@ -17,20 +17,23 @@ describe("gateway session utils", () => {
|
|||||||
expect(res.items).toEqual(["b", "c"]);
|
expect(res.items).toEqual(["b", "c"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parseGroupKey handles group prefixes", () => {
|
test("parseGroupKey handles group keys", () => {
|
||||||
expect(parseGroupKey("group:abc")).toEqual({ id: "abc" });
|
|
||||||
expect(parseGroupKey("discord:group:dev")).toEqual({
|
expect(parseGroupKey("discord:group:dev")).toEqual({
|
||||||
channel: "discord",
|
channel: "discord",
|
||||||
kind: "group",
|
kind: "group",
|
||||||
id: "dev",
|
id: "dev",
|
||||||
});
|
});
|
||||||
|
expect(parseGroupKey("agent:ops:discord:group:dev")).toEqual({
|
||||||
|
channel: "discord",
|
||||||
|
kind: "group",
|
||||||
|
id: "dev",
|
||||||
|
});
|
||||||
expect(parseGroupKey("foo:bar")).toBeNull();
|
expect(parseGroupKey("foo:bar")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("classifySessionKey respects chat type + prefixes", () => {
|
test("classifySessionKey respects chat type + prefixes", () => {
|
||||||
expect(classifySessionKey("global")).toBe("global");
|
expect(classifySessionKey("global")).toBe("global");
|
||||||
expect(classifySessionKey("unknown")).toBe("unknown");
|
expect(classifySessionKey("unknown")).toBe("unknown");
|
||||||
expect(classifySessionKey("group:abc")).toBe("group");
|
|
||||||
expect(classifySessionKey("discord:group:dev")).toBe("group");
|
expect(classifySessionKey("discord:group:dev")).toBe("group");
|
||||||
expect(classifySessionKey("main")).toBe("direct");
|
expect(classifySessionKey("main")).toBe("direct");
|
||||||
const entry = { chatType: "group" } as SessionEntry;
|
const entry = { chatType: "group" } as SessionEntry;
|
||||||
@@ -52,7 +55,9 @@ describe("gateway session utils", () => {
|
|||||||
session: { mainKey: "main" },
|
session: { mainKey: "main" },
|
||||||
agents: { list: [{ id: "ops", default: true }] },
|
agents: { list: [{ id: "ops", default: true }] },
|
||||||
} as ClawdbotConfig;
|
} as ClawdbotConfig;
|
||||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "group:123" })).toBe("agent:ops:group:123");
|
expect(resolveSessionStoreKey({ cfg, sessionKey: "discord:group:123" })).toBe(
|
||||||
|
"agent:ops:discord:group:123",
|
||||||
|
);
|
||||||
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:alpha:main" })).toBe(
|
expect(resolveSessionStoreKey({ cfg, sessionKey: "agent:alpha:main" })).toBe(
|
||||||
"agent:alpha:main",
|
"agent:alpha:main",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function classifySessionKey(key: string, entry?: SessionEntry): GatewaySe
|
|||||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
if (key.startsWith("group:") || key.includes(":group:") || key.includes(":channel:")) {
|
if (key.includes(":group:") || key.includes(":channel:")) {
|
||||||
return "group";
|
return "group";
|
||||||
}
|
}
|
||||||
return "direct";
|
return "direct";
|
||||||
@@ -71,10 +71,6 @@ export function parseGroupKey(
|
|||||||
): { channel?: string; kind?: "group" | "channel"; id?: string } | null {
|
): { channel?: string; kind?: "group" | "channel"; id?: string } | null {
|
||||||
const agentParsed = parseAgentSessionKey(key);
|
const agentParsed = parseAgentSessionKey(key);
|
||||||
const rawKey = agentParsed?.rest ?? key;
|
const rawKey = agentParsed?.rest ?? key;
|
||||||
if (rawKey.startsWith("group:")) {
|
|
||||||
const raw = rawKey.slice("group:".length);
|
|
||||||
return raw ? { id: raw } : null;
|
|
||||||
}
|
|
||||||
const parts = rawKey.split(":").filter(Boolean);
|
const parts = rawKey.split(":").filter(Boolean);
|
||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) {
|
||||||
const [channel, kind, ...rest] = parts;
|
const [channel, kind, ...rest] = parts;
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
|||||||
Body: combinedBody,
|
Body: combinedBody,
|
||||||
RawBody: bodyText,
|
RawBody: bodyText,
|
||||||
CommandBody: bodyText,
|
CommandBody: bodyText,
|
||||||
From: isGroup ? `group:${chatId}` : `imessage:${sender}`,
|
From: isGroup ? `imessage:group:${chatId ?? "unknown"}` : `imessage:${sender}`,
|
||||||
To: imessageTo,
|
To: imessageTo,
|
||||||
SessionKey: route.sessionKey,
|
SessionKey: route.sessionKey,
|
||||||
AccountId: route.accountId,
|
AccountId: route.accountId,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ describe("imessage targets", () => {
|
|||||||
expect(target).toEqual({ kind: "chat_id", chatId: 123 });
|
expect(target).toEqual({ kind: "chat_id", chatId: 123 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses group chat targets", () => {
|
it("parses chat targets", () => {
|
||||||
const target = parseIMessageTarget("group:456");
|
const target = parseIMessageTarget("chat:456");
|
||||||
expect(target).toEqual({ kind: "chat_id", chatId: 456 });
|
expect(target).toEqual({ kind: "chat_id", chatId: 456 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,7 @@ export function parseIMessageTarget(raw: string): IMessageTarget {
|
|||||||
const isChatTarget =
|
const isChatTarget =
|
||||||
CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
||||||
CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
||||||
CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
|
CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p));
|
||||||
remainderLower.startsWith("group:");
|
|
||||||
if (isChatTarget) {
|
if (isChatTarget) {
|
||||||
return parseIMessageTarget(remainder);
|
return parseIMessageTarget(remainder);
|
||||||
}
|
}
|
||||||
@@ -89,16 +88,6 @@ export function parseIMessageTarget(raw: string): IMessageTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.startsWith("group:")) {
|
|
||||||
const value = stripPrefix(trimmed, "group:");
|
|
||||||
const chatId = Number.parseInt(value, 10);
|
|
||||||
if (Number.isFinite(chatId)) {
|
|
||||||
return { kind: "chat_id", chatId };
|
|
||||||
}
|
|
||||||
if (!value) throw new Error("group target is required");
|
|
||||||
return { kind: "chat_guid", chatGuid: value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kind: "handle", to: trimmed, service: "auto" };
|
return { kind: "handle", to: trimmed, service: "auto" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +126,6 @@ export function parseIMessageAllowTarget(raw: string): IMessageAllowTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower.startsWith("group:")) {
|
|
||||||
const value = stripPrefix(trimmed, "group:");
|
|
||||||
const chatId = Number.parseInt(value, 10);
|
|
||||||
if (Number.isFinite(chatId)) return { kind: "chat_id", chatId };
|
|
||||||
if (value) return { kind: "chat_guid", chatGuid: value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kind: "handle", handle: normalizeIMessageHandle(trimmed) };
|
return { kind: "handle", handle: normalizeIMessageHandle(trimmed) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ describe("resolveHeartbeatDeliveryTarget", () => {
|
|||||||
const entry = {
|
const entry = {
|
||||||
...baseEntry,
|
...baseEntry,
|
||||||
lastChannel: "whatsapp" as const,
|
lastChannel: "whatsapp" as const,
|
||||||
lastTo: "whatsapp:group:120363401234567890@G.US",
|
lastTo: "whatsapp:120363401234567890@G.US",
|
||||||
};
|
};
|
||||||
expect(resolveHeartbeatDeliveryTarget({ cfg, entry })).toEqual({
|
expect(resolveHeartbeatDeliveryTarget({ cfg, entry })).toEqual({
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe("runMessageAction context isolation", () => {
|
|||||||
action: "send",
|
action: "send",
|
||||||
params: {
|
params: {
|
||||||
channel: "whatsapp",
|
channel: "whatsapp",
|
||||||
target: "group:123@g.us",
|
target: "123@g.us",
|
||||||
message: "hi",
|
message: "hi",
|
||||||
},
|
},
|
||||||
toolContext: { currentChannelId: "123@g.us" },
|
toolContext: { currentChannelId: "123@g.us" },
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe("resolveOutboundTarget", () => {
|
|||||||
name: "normalizes prefixed/uppercase whatsapp group targets",
|
name: "normalizes prefixed/uppercase whatsapp group targets",
|
||||||
input: {
|
input: {
|
||||||
channel: "whatsapp" as const,
|
channel: "whatsapp" as const,
|
||||||
to: " WhatsApp:Group:120363401234567890@G.US ",
|
to: " WhatsApp:120363401234567890@G.US ",
|
||||||
},
|
},
|
||||||
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
expected: { ok: true as const, to: "120363401234567890@g.us" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -61,7 +61,15 @@ function isSurfaceGroupKey(key: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLegacyGroupKey(key: string): boolean {
|
function isLegacyGroupKey(key: string): boolean {
|
||||||
return key.startsWith("group:") || key.includes("@g.us");
|
const trimmed = key.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
if (trimmed.startsWith("group:")) return true;
|
||||||
|
const lower = trimmed.toLowerCase();
|
||||||
|
if (!lower.includes("@g.us")) return false;
|
||||||
|
// Legacy WhatsApp group keys: bare JID or "whatsapp:<jid>" without explicit ":group:" kind.
|
||||||
|
if (!trimmed.includes(":")) return true;
|
||||||
|
if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) return true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSessionKeyForAgent(key: string, agentId: string): string {
|
function normalizeSessionKeyForAgent(key: string, agentId: string): string {
|
||||||
@@ -72,6 +80,22 @@ function normalizeSessionKeyForAgent(key: string, agentId: string): string {
|
|||||||
const rest = raw.slice("subagent:".length);
|
const rest = raw.slice("subagent:".length);
|
||||||
return `agent:${normalizeAgentId(agentId)}:subagent:${rest}`;
|
return `agent:${normalizeAgentId(agentId)}:subagent:${rest}`;
|
||||||
}
|
}
|
||||||
|
if (raw.startsWith("group:")) {
|
||||||
|
const id = raw.slice("group:".length).trim();
|
||||||
|
if (!id) return raw;
|
||||||
|
const channel = id.toLowerCase().includes("@g.us") ? "whatsapp" : "unknown";
|
||||||
|
return `agent:${normalizeAgentId(agentId)}:${channel}:group:${id}`;
|
||||||
|
}
|
||||||
|
if (!raw.includes(":") && raw.toLowerCase().includes("@g.us")) {
|
||||||
|
return `agent:${normalizeAgentId(agentId)}:whatsapp:group:${raw}`;
|
||||||
|
}
|
||||||
|
if (raw.toLowerCase().startsWith("whatsapp:") && raw.toLowerCase().includes("@g.us")) {
|
||||||
|
const remainder = raw.slice("whatsapp:".length).trim();
|
||||||
|
const cleaned = remainder.replace(/^group:/i, "").trim();
|
||||||
|
if (cleaned && !isSurfaceGroupKey(raw)) {
|
||||||
|
return `agent:${normalizeAgentId(agentId)}:whatsapp:group:${cleaned}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isSurfaceGroupKey(raw)) {
|
if (isSurfaceGroupKey(raw)) {
|
||||||
return `agent:${normalizeAgentId(agentId)}:${raw}`;
|
return `agent:${normalizeAgentId(agentId)}:${raw}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function deriveChannelFromKey(key?: string) {
|
|||||||
|
|
||||||
function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
|
function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
|
||||||
if (!key) return undefined;
|
if (!key) return undefined;
|
||||||
if (key.startsWith("group:") || key.includes(":group:")) return "group";
|
if (key.includes(":group:")) return "group";
|
||||||
if (key.includes(":channel:")) return "channel";
|
if (key.includes(":channel:")) return "channel";
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ describe("createTelegramBot", () => {
|
|||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
const payload = replySpy.mock.calls[0][0];
|
const payload = replySpy.mock.calls[0][0];
|
||||||
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
||||||
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
expect(payload.From).toBe("telegram:group:-1001234567890:topic:99");
|
||||||
expect(payload.MessageThreadId).toBe(99);
|
expect(payload.MessageThreadId).toBe(99);
|
||||||
expect(payload.IsForum).toBe(true);
|
expect(payload.IsForum).toBe(true);
|
||||||
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
||||||
|
|||||||
@@ -1815,7 +1815,7 @@ describe("createTelegramBot", () => {
|
|||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
const payload = replySpy.mock.calls[0][0];
|
const payload = replySpy.mock.calls[0][0];
|
||||||
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
expect(payload.SessionKey).toContain("telegram:group:-1001234567890:topic:99");
|
||||||
expect(payload.From).toBe("group:-1001234567890:topic:99");
|
expect(payload.From).toBe("telegram:group:-1001234567890:topic:99");
|
||||||
expect(payload.MessageThreadId).toBe(99);
|
expect(payload.MessageThreadId).toBe(99);
|
||||||
expect(payload.IsForum).toBe(true);
|
expect(payload.IsForum).toBe(true);
|
||||||
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
expect(sendChatActionSpy).toHaveBeenCalledWith(-1001234567890, "typing", {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function buildTelegramGroupPeerId(chatId: number | string, messageThreadI
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
|
export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
|
||||||
return messageThreadId != null ? `group:${chatId}:topic:${messageThreadId}` : `group:${chatId}`;
|
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildSenderName(msg: TelegramMessage) {
|
export function buildSenderName(msg: TelegramMessage) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ describe("applyGroupGating", () => {
|
|||||||
sendMedia: async () => {},
|
sendMedia: async () => {},
|
||||||
},
|
},
|
||||||
conversationId: "123@g.us",
|
conversationId: "123@g.us",
|
||||||
groupHistoryKey: "group:123@g.us",
|
groupHistoryKey: "whatsapp:default:group:123@g.us",
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
||||||
baseMentionConfig: { mentionRegexes: [] },
|
baseMentionConfig: { mentionRegexes: [] },
|
||||||
|
|||||||
@@ -9,15 +9,6 @@ describe("normalizeWhatsAppTarget", () => {
|
|||||||
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
|
expect(normalizeWhatsAppTarget("whatsapp:120363401234567890@g.us")).toBe(
|
||||||
"120363401234567890@g.us",
|
"120363401234567890@g.us",
|
||||||
);
|
);
|
||||||
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBe(
|
|
||||||
"120363401234567890@g.us",
|
|
||||||
);
|
|
||||||
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBe(
|
|
||||||
"123456789-987654321@g.us",
|
|
||||||
);
|
|
||||||
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBe(
|
|
||||||
"123456789-987654321@g.us",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes direct JIDs to E.164", () => {
|
it("normalizes direct JIDs to E.164", () => {
|
||||||
@@ -43,12 +34,15 @@ describe("normalizeWhatsAppTarget", () => {
|
|||||||
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
|
expect(normalizeWhatsAppTarget("whatsapp:")).toBeNull();
|
||||||
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
|
expect(normalizeWhatsAppTarget("@g.us")).toBeNull();
|
||||||
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
|
expect(normalizeWhatsAppTarget("whatsapp:group:@g.us")).toBeNull();
|
||||||
|
expect(normalizeWhatsAppTarget("whatsapp:group:120363401234567890@g.us")).toBeNull();
|
||||||
|
expect(normalizeWhatsAppTarget("group:123456789-987654321@g.us")).toBeNull();
|
||||||
|
expect(normalizeWhatsAppTarget(" WhatsApp:Group:123456789-987654321@G.US ")).toBeNull();
|
||||||
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
|
expect(normalizeWhatsAppTarget("abc@s.whatsapp.net")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles repeated prefixes", () => {
|
it("handles repeated prefixes", () => {
|
||||||
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
|
expect(normalizeWhatsAppTarget("whatsapp:whatsapp:+1555")).toBe("+1555");
|
||||||
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBe("120@g.us");
|
expect(normalizeWhatsAppTarget("group:group:120@g.us")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +64,7 @@ describe("isWhatsAppGroupJid", () => {
|
|||||||
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
expect(isWhatsAppGroupJid("120363401234567890@g.us")).toBe(true);
|
||||||
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
|
expect(isWhatsAppGroupJid("123456789-987654321@g.us")).toBe(true);
|
||||||
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
|
expect(isWhatsAppGroupJid("whatsapp:120363401234567890@g.us")).toBe(true);
|
||||||
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(true);
|
expect(isWhatsAppGroupJid("whatsapp:group:120363401234567890@g.us")).toBe(false);
|
||||||
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
|
expect(isWhatsAppGroupJid("x@g.us")).toBe(false);
|
||||||
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
|
expect(isWhatsAppGroupJid("@g.us")).toBe(false);
|
||||||
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
|
expect(isWhatsAppGroupJid("120@g.usx")).toBe(false);
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ function stripWhatsAppTargetPrefixes(value: string): string {
|
|||||||
let candidate = value.trim();
|
let candidate = value.trim();
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const before = candidate;
|
const before = candidate;
|
||||||
candidate = candidate
|
candidate = candidate.replace(/^whatsapp:/i, "").trim();
|
||||||
.replace(/^whatsapp:/i, "")
|
|
||||||
.replace(/^group:/i, "")
|
|
||||||
.trim();
|
|
||||||
if (candidate === before) return candidate;
|
if (candidate === before) return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +56,9 @@ export function normalizeWhatsAppTarget(value: string): string | null {
|
|||||||
const normalized = normalizeE164(phone);
|
const normalized = normalizeE164(phone);
|
||||||
return normalized.length > 1 ? normalized : null;
|
return normalized.length > 1 ? normalized : null;
|
||||||
}
|
}
|
||||||
|
// If the caller passed a JID-ish string that we don't understand, fail fast.
|
||||||
|
// Otherwise normalizeE164 would happily treat "group:120@g.us" as a phone number.
|
||||||
|
if (candidate.includes("@")) return null;
|
||||||
const normalized = normalizeE164(candidate);
|
const normalized = normalizeE164(candidate);
|
||||||
return normalized.length > 1 ? normalized : null;
|
return normalized.length > 1 ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user