fix: canonicalize legacy whatsapp group sessions

This commit is contained in:
Peter Steinberger
2026-04-05 05:46:53 +01:00
parent bf0f4d93f0
commit 50b5c483ee
5 changed files with 56 additions and 32 deletions

View File

@@ -3,7 +3,9 @@ type UnsupportedSecretRefConfigCandidate = {
value: unknown;
};
export { normalizeCompatibilityConfig } from "./src/doctor-contract.js";
import { hasAnyWhatsAppAuth } from "./src/accounts.js";
export { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./src/session-contract.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./session-contract.js";
describe("whatsapp legacy session contract", () => {
it("canonicalizes legacy WhatsApp group keys to channel-qualified agent keys", () => {
expect(canonicalizeLegacySessionKey({ key: "group:123@g.us", agentId: "main" })).toBe(
"agent:main:whatsapp:group:123@g.us",
);
expect(canonicalizeLegacySessionKey({ key: "123@g.us", agentId: "main" })).toBe(
"agent:main:whatsapp:group:123@g.us",
);
expect(canonicalizeLegacySessionKey({ key: "whatsapp:123@g.us", agentId: "main" })).toBe(
"agent:main:whatsapp:group:123@g.us",
);
});
it("does not claim generic non-WhatsApp group keys", () => {
expect(isLegacyGroupSessionKey("group:abc")).toBe(false);
expect(canonicalizeLegacySessionKey({ key: "group:abc", agentId: "main" })).toBeNull();
});
});

View File

@@ -1,42 +1,37 @@
export function isLegacyGroupSessionKey(key: string): boolean {
function extractLegacyWhatsAppGroupId(key: string): string | null {
const trimmed = key.trim();
if (!trimmed) {
return false;
}
if (trimmed.startsWith("group:")) {
return true;
return null;
}
const lower = trimmed.toLowerCase();
if (trimmed.startsWith("group:")) {
const id = trimmed.slice("group:".length).trim();
return id.toLowerCase().includes("@g.us") ? id : null;
}
if (!lower.includes("@g.us")) {
return false;
return null;
}
if (!trimmed.includes(":")) {
return true;
return trimmed;
}
return lower.startsWith("whatsapp:") && !trimmed.includes(":group:");
if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) {
const remainder = trimmed.slice("whatsapp:".length).trim();
const cleaned = remainder.replace(/^group:/i, "").trim();
return cleaned || null;
}
return null;
}
export function isLegacyGroupSessionKey(key: string): boolean {
return extractLegacyWhatsAppGroupId(key) !== null;
}
export function canonicalizeLegacySessionKey(params: {
key: string;
agentId: string;
}): string | null {
const trimmed = params.key.trim();
if (!trimmed) {
return null;
}
if (trimmed.startsWith("group:")) {
const id = trimmed.slice("group:".length).trim();
return id ? `agent:${params.agentId}:whatsapp:group:${id}`.toLowerCase() : null;
}
if (!trimmed.includes(":") && trimmed.toLowerCase().includes("@g.us")) {
return `agent:${params.agentId}:whatsapp:group:${trimmed}`.toLowerCase();
}
if (trimmed.toLowerCase().startsWith("whatsapp:") && trimmed.toLowerCase().includes("@g.us")) {
const remainder = trimmed.slice("whatsapp:".length).trim();
const cleaned = remainder.replace(/^group:/i, "").trim();
if (cleaned && !trimmed.includes(":group:")) {
return `agent:${params.agentId}:whatsapp:group:${cleaned}`.toLowerCase();
}
}
return null;
const legacyGroupId = extractLegacyWhatsAppGroupId(params.key);
return legacyGroupId
? `agent:${params.agentId}:whatsapp:group:${legacyGroupId}`.toLowerCase()
: null;
}

View File

@@ -99,17 +99,15 @@ describe("state migrations", () => {
expect(detected.agentDir.hasLegacy).toBe(true);
expect(detected.channelPlans.hasLegacy).toBe(true);
expect(detected.channelPlans.plans.map((plan) => plan.targetPath)).toEqual([
path.join(stateDir, "credentials", "whatsapp", "default", "creds.json"),
path.join(stateDir, "credentials", "whatsapp", "default", "pre-key-1.json"),
resolveChannelAllowFromPath("telegram", env, "alpha"),
path.join(stateDir, "credentials", "whatsapp", "default", "creds.json"),
]);
expect(detected.preview).toEqual([
`- Sessions: ${path.join(stateDir, "sessions")}${path.join(stateDir, "agents", "worker-1", "sessions")}`,
`- Sessions: canonicalize legacy keys in ${path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json")}`,
`- Agent dir: ${path.join(stateDir, "agent")}${path.join(stateDir, "agents", "worker-1", "agent")}`,
`- WhatsApp auth creds.json: ${path.join(stateDir, "credentials", "creds.json")}${path.join(stateDir, "credentials", "whatsapp", "default", "creds.json")}`,
`- WhatsApp auth pre-key-1.json: ${path.join(stateDir, "credentials", "pre-key-1.json")}${path.join(stateDir, "credentials", "whatsapp", "default", "pre-key-1.json")}`,
`- Telegram pairing allowFrom: ${resolveChannelAllowFromPath("telegram", env)}${resolveChannelAllowFromPath("telegram", env, "alpha")}`,
`- WhatsApp auth creds.json: ${path.join(stateDir, "credentials", "creds.json")}${path.join(stateDir, "credentials", "whatsapp", "default", "creds.json")}`,
]);
});
@@ -133,9 +131,9 @@ describe("state migrations", () => {
"Canonicalized 1 legacy session key(s)",
"Moved trace.jsonl → agents/worker-1/sessions",
"Moved agent file settings.json → agents/worker-1/agent",
`Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`,
`Moved WhatsApp auth creds.json → ${path.join(stateDir, "credentials", "whatsapp", "default", "creds.json")}`,
`Moved WhatsApp auth pre-key-1.json → ${path.join(stateDir, "credentials", "whatsapp", "default", "pre-key-1.json")}`,
`Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`,
]);
const mergedStore = JSON.parse(

View File

@@ -93,6 +93,10 @@ function isLegacyGroupKey(key: string): boolean {
if (!trimmed) {
return false;
}
const lower = trimmed.toLowerCase();
if (lower.startsWith("group:") || lower.startsWith("channel:")) {
return true;
}
for (const surface of getLegacySessionSurfaces()) {
if (surface.isLegacyGroupSessionKey?.(trimmed)) {
return true;
@@ -215,6 +219,10 @@ function canonicalizeSessionKeyForAgent(params: {
return canonicalized.trim().toLowerCase();
}
}
const lower = raw.toLowerCase();
if (lower.startsWith("group:") || lower.startsWith("channel:")) {
return `agent:${agentId}:unknown:${raw}`.toLowerCase();
}
if (isSurfaceGroupKey(raw)) {
return `agent:${agentId}:${raw}`.toLowerCase();
}