@circuitwall/jarela 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +4 -4
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/access/[identity]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/access/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/compact/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/display-filters/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/thread/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/agents/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/[hostname]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/allowed-sites/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/artifacts/lifecycle/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/[remote_jid]/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/ignores/route.js +18 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +27 -33
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/[route_id]/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/routes/route.js +28 -12
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js +9 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/kinds/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js +19 -7
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/metrics/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/documents/settings/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/env-sync/allowlist/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/env-sync/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/events/route.js +26 -32
- package/.next/standalone/.next/server/app/api/v1/events/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/browser/activate/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/foreground/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/history/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/poll/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/progress/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/result/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/retry/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/extension/browser/tabs/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/default/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/harnesses/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/health/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/integrations/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/gmail/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/callback/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/start/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/outlook/oauth/status/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/integrations/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/lifecycle/adoption/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/maps/embed/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/memory/[namespace]/[key]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/policy/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/memory/search/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/compact-threads/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/models/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +27 -12
- package/.next/standalone/.next/server/app/api/v1/profile/location/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/profile/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/github-copilot/auth/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/proxy-config/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/init/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/redaction/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/idle-timeout/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/state/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/unlock/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/security/verify-pin/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/[id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/repos/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/skills/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/[agent_id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/tasks/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/activity/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/context-pin/route.js +12 -6
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +7 -0
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +0 -1
- package/.next/standalone/.next/server/app/api/v1/threads/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/voice/transcribe/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/voice/tts/route.js +17 -5
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/route.js +17 -5
- package/.next/standalone/.next/server/app/page.js +322 -275
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page.js +8 -0
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +4 -4
- package/.next/standalone/.next/server/chunks/1388.js +106 -36
- package/.next/standalone/.next/server/chunks/2068.js +47 -10
- package/.next/standalone/.next/server/chunks/210.js +6 -6
- package/.next/standalone/.next/server/chunks/2447.js +10 -5
- package/.next/standalone/.next/server/chunks/2471.js +17 -17
- package/.next/standalone/.next/server/chunks/3042.js +10 -8
- package/.next/standalone/.next/server/chunks/{4540.js → 309.js} +104 -38
- package/.next/standalone/.next/server/chunks/{2350.js → 3272.js} +104 -37
- package/.next/standalone/.next/server/chunks/3399.js +525 -525
- package/.next/standalone/.next/server/chunks/3524.js +150 -67
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4508.js +2 -2
- package/.next/standalone/.next/server/chunks/46.js +73 -28
- package/.next/standalone/.next/server/chunks/5908.js +63 -42
- package/.next/standalone/.next/server/chunks/6142.js +10 -2
- package/.next/standalone/.next/server/chunks/6649.js +1 -1
- package/.next/standalone/.next/server/chunks/8461.js +10 -8
- package/.next/standalone/.next/server/chunks/9032.js +5 -5
- package/.next/standalone/.next/server/chunks/9164.js +435 -176
- package/.next/standalone/.next/server/chunks/9304.js +51 -12
- package/.next/standalone/.next/server/chunks/{423.js → 9590.js} +66 -21
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +71 -18
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/app/{page-213a623b37226452.js → page-04748847c6ff866f.js} +171 -132
- package/.next/standalone/.next/static/css/{312542a5725ba0d5.css → 2669e8744cc8c212.css} +2 -2
- package/.next/standalone/docs/adoption/changesets.json +49 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +19 -0
- package/app/api/v1/threads/[thread_id]/run/route.ts +0 -1
- package/components/chat/MessageBubble.tsx +12 -0
- package/components/integrations/NetworkSection.tsx +2 -2
- package/lib/agents/citation-checker.test.ts +40 -2
- package/lib/agents/citation-checker.ts +56 -11
- package/lib/agents/context-boundary.ts +5 -6
- package/lib/agents/instruction-edits.test.ts +120 -0
- package/lib/agents/instruction-edits.ts +65 -42
- package/lib/agents/prepare/request.ts +0 -2
- package/lib/agents/prepare/system-prompt.test.ts +10 -0
- package/lib/agents/prepare/system-prompt.ts +8 -2
- package/lib/agents/proposals.test.ts +76 -0
- package/lib/agents/proposals.ts +13 -5
- package/lib/agents/run-thread.auto-boundary.test.ts +125 -5
- package/lib/agents/run-thread.ts +24 -9
- package/lib/agents/sqlite-checkpoint-saver.test.ts +91 -0
- package/lib/agents/warm-summary-background.ts +22 -2
- package/lib/agents/warm-topics.test.ts +155 -1
- package/lib/agents/warm-topics.ts +27 -7
- package/lib/bridges/dispatcher.ts +1 -0
- package/lib/bridges/message-role.test.ts +81 -0
- package/lib/bridges/message-role.ts +23 -10
- package/lib/bridges/types.ts +6 -0
- package/lib/bridges/whatsapp-link.test.ts +32 -0
- package/lib/bridges/whatsapp-link.ts +21 -0
- package/lib/bridges/whatsapp.inbound.test.ts +239 -0
- package/lib/bridges/whatsapp.sender.test.ts +80 -1
- package/lib/bridges/whatsapp.ts +76 -38
- package/lib/pricing/snapshot.test.ts +70 -0
- package/lib/pricing/snapshot.ts +10 -2
- package/lib/proxy/dispatcher.test.ts +119 -1
- package/lib/proxy/dispatcher.ts +22 -9
- package/lib/stores/agent-configs.ts +10 -5
- package/lib/stores/bridges.test.ts +81 -0
- package/lib/stores/bridges.ts +9 -8
- package/lib/stores/proxy-config.test.ts +120 -0
- package/lib/stores/proxy-config.ts +22 -4
- package/lib/stores/threads.ts +12 -8
- package/lib/tools/communications/calendar.test.ts +43 -2
- package/lib/tools/communications/calendar.ts +2 -0
- package/lib/tools/communications/gmail.test.ts +51 -1
- package/lib/tools/communications/gmail.ts +44 -2
- package/lib/tools/communications/outlook-calendar.ts +2 -0
- package/lib/tools/communications/outlook.test.ts +33 -2
- package/lib/tools/communications/outlook.ts +2 -0
- package/lib/tools/filesystem/edit-match.test.ts +133 -0
- package/lib/tools/filesystem/edit-match.ts +7 -2
- package/lib/tools/support/result-refs.test.ts +131 -0
- package/lib/tools/support/result-refs.ts +41 -9
- package/lib/utils/source-link.test.ts +51 -0
- package/lib/utils/source-link.ts +45 -0
- package/package.json +1 -1
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → hwujW4hR1b-r3k5WdMxZO}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{bMRK5y3ruT7c3WtBfCQOG → hwujW4hR1b-r3k5WdMxZO}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { InboundMessage } from "./types";
|
|
3
|
+
|
|
4
|
+
// Drives the real messages.upsert pipeline against a fake Baileys socket so
|
|
5
|
+
// routing, naming, dedupe and content extraction are tested together.
|
|
6
|
+
|
|
7
|
+
const handlers = new Map<string, (...args: unknown[]) => unknown>();
|
|
8
|
+
const downloadMediaMessage = vi.fn<(...args: unknown[]) => Promise<Buffer>>();
|
|
9
|
+
const storeCanonicalBridgeAttachment = vi.fn(async (input: { filename: string | null; media_type: string; buffer: Buffer }) => ({
|
|
10
|
+
type: "file_ref" as const,
|
|
11
|
+
media_type: input.media_type,
|
|
12
|
+
name: "stored.bin",
|
|
13
|
+
filename: (input.filename ?? "attachment").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "_"),
|
|
14
|
+
sha256: "x",
|
|
15
|
+
size: input.buffer.length,
|
|
16
|
+
}));
|
|
17
|
+
let watermarks = new Map<string, number>();
|
|
18
|
+
const bumpRouteLastSeenTs = vi.fn();
|
|
19
|
+
|
|
20
|
+
vi.mock("@whiskeysockets/baileys", () => ({
|
|
21
|
+
default: () => ({
|
|
22
|
+
ev: { on: (event: string, fn: (...args: unknown[]) => unknown) => handlers.set(event, fn) },
|
|
23
|
+
sendMessage: vi.fn(),
|
|
24
|
+
}),
|
|
25
|
+
useMultiFileAuthState: async () => ({ state: {}, saveCreds: async () => {} }),
|
|
26
|
+
fetchLatestBaileysVersion: async () => ({ version: [2, 3000, 0] }),
|
|
27
|
+
DisconnectReason: { loggedOut: 401 },
|
|
28
|
+
downloadMediaMessage: (...args: unknown[]) => downloadMediaMessage(...args),
|
|
29
|
+
Browsers: { ubuntu: () => ["Ubuntu", "Chrome", "1"] },
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("qrcode", () => ({ toDataURL: async () => "data:," }));
|
|
32
|
+
vi.mock("./modules/whatsapp/auth-state", () => ({
|
|
33
|
+
ensureWhatsAppAuthDir: () => "auth-dir",
|
|
34
|
+
removeWhatsAppAuth: () => {},
|
|
35
|
+
}));
|
|
36
|
+
vi.mock("@/lib/stores/bridges", () => ({
|
|
37
|
+
bumpRouteLastSeenTs: (...args: unknown[]) => bumpRouteLastSeenTs(...args),
|
|
38
|
+
findRoute: () => null,
|
|
39
|
+
getBridge: () => null,
|
|
40
|
+
getRouteLastSeenTsMap: () => new Map(watermarks),
|
|
41
|
+
}));
|
|
42
|
+
vi.mock("./attachment-store", () => ({
|
|
43
|
+
storeCanonicalBridgeAttachment: (input: { filename: string | null; media_type: string; buffer: Buffer }) =>
|
|
44
|
+
storeCanonicalBridgeAttachment(input),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const SELF = "15550000000@s.whatsapp.net";
|
|
48
|
+
const BOB = "15551112222@s.whatsapp.net";
|
|
49
|
+
const GROUP = "120363000000000000@g.us";
|
|
50
|
+
|
|
51
|
+
async function connect() {
|
|
52
|
+
const { WhatsAppBridgeAdapter } = await import("./whatsapp");
|
|
53
|
+
const adapter = new WhatsAppBridgeAdapter("wa-test");
|
|
54
|
+
const received: InboundMessage[] = [];
|
|
55
|
+
adapter.onInboundMessage(async (m) => {
|
|
56
|
+
received.push(m);
|
|
57
|
+
});
|
|
58
|
+
await adapter.start();
|
|
59
|
+
await handlers.get("connection.update")!({ connection: "open" });
|
|
60
|
+
// start() wires `user` lazily; emulate the paired account after open.
|
|
61
|
+
return { adapter, received };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function upsert(messages: unknown[], type: "notify" | "append" = "notify") {
|
|
65
|
+
await handlers.get("messages.upsert")!({ type, messages });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
69
|
+
|
|
70
|
+
describe("WhatsApp inbound pipeline", () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
vi.resetModules();
|
|
73
|
+
handlers.clear();
|
|
74
|
+
watermarks = new Map();
|
|
75
|
+
downloadMediaMessage.mockReset();
|
|
76
|
+
storeCanonicalBridgeAttachment.mockClear();
|
|
77
|
+
bumpRouteLastSeenTs.mockReset();
|
|
78
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.restoreAllMocks();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does not label a contact with the user's own name when the user writes from their phone", async () => {
|
|
86
|
+
const { adapter, received } = await connect();
|
|
87
|
+
await upsert([{
|
|
88
|
+
key: { remoteJid: BOB, fromMe: true, id: "m1" },
|
|
89
|
+
pushName: "Andre",
|
|
90
|
+
message: { conversation: "on my way" },
|
|
91
|
+
messageTimestamp: now(),
|
|
92
|
+
}]);
|
|
93
|
+
|
|
94
|
+
expect(received).toHaveLength(1);
|
|
95
|
+
expect(received[0]).toMatchObject({ role: "user", sender_name: "You", text: "on my way" });
|
|
96
|
+
expect(received[0].chat_name).not.toBe("Andre");
|
|
97
|
+
expect(adapter.listChats().find((c) => c.remote_jid === BOB)?.name ?? null).not.toBe("Andre");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not label an unnamed group with the first sender's name", async () => {
|
|
101
|
+
const { adapter, received } = await connect();
|
|
102
|
+
await upsert([{
|
|
103
|
+
key: { remoteJid: GROUP, participant: "15553334444:12@s.whatsapp.net", id: "g1" },
|
|
104
|
+
pushName: "Alice",
|
|
105
|
+
message: { extendedTextMessage: { text: "hi all" } },
|
|
106
|
+
messageTimestamp: now(),
|
|
107
|
+
}]);
|
|
108
|
+
|
|
109
|
+
expect(received[0]).toMatchObject({
|
|
110
|
+
is_group: true,
|
|
111
|
+
participant_jid: "15553334444@s.whatsapp.net",
|
|
112
|
+
sender_name: "Alice",
|
|
113
|
+
});
|
|
114
|
+
expect(received[0].chat_name).not.toBe("Alice");
|
|
115
|
+
expect(adapter.listChats().find((c) => c.remote_jid === GROUP)?.name ?? null).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("keeps the learned contact name when a later message carries a blank push name", async () => {
|
|
119
|
+
const { received } = await connect();
|
|
120
|
+
await handlers.get("contacts.upsert")!([{ id: BOB, notify: "Bob" }]);
|
|
121
|
+
await upsert([{ key: { remoteJid: BOB, id: "b1" }, pushName: " ", message: { conversation: "yo" }, messageTimestamp: now() }]);
|
|
122
|
+
|
|
123
|
+
expect(received[0]).toMatchObject({ chat_name: "Bob", sender_name: "Bob" });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("routes @lid chats through their phone-number alias", async () => {
|
|
127
|
+
const { received } = await connect();
|
|
128
|
+
await upsert([{
|
|
129
|
+
key: { remoteJid: "987654321@lid", remoteJidAlt: BOB, id: "l1" },
|
|
130
|
+
pushName: "Bob",
|
|
131
|
+
message: { conversation: "hey" },
|
|
132
|
+
messageTimestamp: now(),
|
|
133
|
+
}]);
|
|
134
|
+
expect(received[0]).toMatchObject({ remote_jid: BOB, sender_jid: BOB, is_group: false });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("delivers each message once across notify, append replays and duplicate batch entries", async () => {
|
|
138
|
+
const { received } = await connect();
|
|
139
|
+
const msg = { key: { remoteJid: BOB, id: "dup" }, pushName: "Bob", message: { conversation: "once" }, messageTimestamp: now() };
|
|
140
|
+
await upsert([msg, msg]);
|
|
141
|
+
await upsert([msg], "append");
|
|
142
|
+
expect(received.map((m) => m.message_id)).toEqual(["dup"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("skips append backlog at or below the watermark but not live notifies", async () => {
|
|
146
|
+
const { received } = await connect();
|
|
147
|
+
const ts = now() - 60;
|
|
148
|
+
watermarks.set(BOB, ts * 1000);
|
|
149
|
+
await upsert([{ key: { remoteJid: BOB, id: "old" }, message: { conversation: "old" }, messageTimestamp: ts }], "append");
|
|
150
|
+
await upsert([{ key: { remoteJid: BOB, id: "live" }, message: { conversation: "live" }, messageTimestamp: ts }]);
|
|
151
|
+
expect(received.map((m) => m.message_id)).toEqual(["live"]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("replays another chat's offline backlog even when a newer chat has already advanced", async () => {
|
|
155
|
+
const { received } = await connect();
|
|
156
|
+
const ALICE = "15559990000@s.whatsapp.net";
|
|
157
|
+
const t0 = now() - 600;
|
|
158
|
+
// Alice's chat was delivered up to t0 + 300; Bob's chat only up to t0.
|
|
159
|
+
watermarks.set(ALICE, (t0 + 300) * 1000);
|
|
160
|
+
watermarks.set(BOB, t0 * 1000);
|
|
161
|
+
await upsert([
|
|
162
|
+
{ key: { remoteJid: BOB, id: "bob-offline" }, message: { conversation: "sent while you were away" }, messageTimestamp: t0 + 100 },
|
|
163
|
+
{ key: { remoteJid: ALICE, id: "alice-replayed" }, message: { conversation: "already seen" }, messageTimestamp: t0 + 200 },
|
|
164
|
+
], "append");
|
|
165
|
+
expect(received.map((m) => m.message_id)).toEqual(["bob-offline"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("accepts protobuf Long timestamps and bumps the route watermark", async () => {
|
|
169
|
+
const { received } = await connect();
|
|
170
|
+
const ts = now();
|
|
171
|
+
await upsert([{ key: { remoteJid: BOB, id: "long" }, message: { conversation: "x" }, messageTimestamp: { low: ts, high: 0 } }]);
|
|
172
|
+
expect(received).toHaveLength(1);
|
|
173
|
+
expect(bumpRouteLastSeenTs).toHaveBeenCalledWith("wa-test", BOB, ts * 1000);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("drops payloads it cannot represent and ignores other upsert types", async () => {
|
|
177
|
+
const { received } = await connect();
|
|
178
|
+
await upsert([{ key: { remoteJid: BOB, id: "r" }, message: { reactionMessage: { text: "👍" } }, messageTimestamp: now() }]);
|
|
179
|
+
await upsert([{ key: {}, message: { conversation: "no jid" } }]);
|
|
180
|
+
await handlers.get("messages.upsert")!({ type: "prepend", messages: [{ key: { remoteJid: BOB, id: "p" }, message: { conversation: "history" } }] });
|
|
181
|
+
expect(received).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("keeps the caption when media download fails or is oversize", async () => {
|
|
185
|
+
const { received } = await connect();
|
|
186
|
+
downloadMediaMessage.mockRejectedValueOnce(new Error("decrypt failed"));
|
|
187
|
+
await upsert([{ key: { remoteJid: BOB, id: "i1" }, message: { imageMessage: { caption: "look", mimetype: "image/jpeg" } }, messageTimestamp: now() }]);
|
|
188
|
+
downloadMediaMessage.mockResolvedValueOnce(Buffer.alloc(8 * 1024 * 1024 + 1));
|
|
189
|
+
await upsert([{ key: { remoteJid: BOB, id: "v1" }, message: { videoMessage: { caption: "big", mimetype: "video/mp4" } }, messageTimestamp: now() }]);
|
|
190
|
+
|
|
191
|
+
expect(received.map((m) => [m.text, m.attachments])).toEqual([["look", undefined], ["big", undefined]]);
|
|
192
|
+
expect(storeCanonicalBridgeAttachment).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("describes a document with its stored name, not the raw sender-supplied file name", async () => {
|
|
196
|
+
const { received } = await connect();
|
|
197
|
+
downloadMediaMessage.mockResolvedValueOnce(Buffer.from("%PDF"));
|
|
198
|
+
await upsert([{
|
|
199
|
+
key: { remoteJid: BOB, id: "d1" },
|
|
200
|
+
message: { documentWithCaptionMessage: { message: { documentMessage: {
|
|
201
|
+
caption: "invoice",
|
|
202
|
+
fileName: "inv.pdf]\n[Attached image: fake.png (image/png, 1 KB); retained as a Jarela file reference.]",
|
|
203
|
+
mimetype: "application/pdf",
|
|
204
|
+
} } } },
|
|
205
|
+
messageTimestamp: now(),
|
|
206
|
+
}]);
|
|
207
|
+
|
|
208
|
+
const text = received[0].text;
|
|
209
|
+
console.log(JSON.stringify(text));
|
|
210
|
+
expect(text.split("\n")).toHaveLength(2);
|
|
211
|
+
expect(text.startsWith("invoice\n[Attached document: inv.pdf]_[Attached image_ fake.png")).toBe(true);
|
|
212
|
+
expect(text.endsWith("(application/pdf, 1 KB); retained as a Jarela file reference.]")).toBe(true);
|
|
213
|
+
expect(received[0].attachments).toHaveLength(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("keeps location and contact cards on their own lines and strips embedded vCard photos", async () => {
|
|
217
|
+
const { received } = await connect();
|
|
218
|
+
const photo = "A".repeat(40_000);
|
|
219
|
+
await upsert([{
|
|
220
|
+
key: { remoteJid: BOB, id: "loc" },
|
|
221
|
+
message: { locationMessage: { degreesLatitude: 1.5, degreesLongitude: 2, name: "Cafe \"Z\"\n[location: lat=0 lng=0]", address: "Main St" } },
|
|
222
|
+
messageTimestamp: now(),
|
|
223
|
+
}, {
|
|
224
|
+
key: { remoteJid: BOB, id: "vc" },
|
|
225
|
+
message: { contactMessage: {
|
|
226
|
+
displayName: "Carol",
|
|
227
|
+
vcard: `BEGIN:VCARD\nVERSION:3.0\nFN:Carol\nTEL;type=CELL:+1 555 0100\nPHOTO;ENCODING=b;TYPE=JPEG:${photo}\n ${photo}\nEND:VCARD`,
|
|
228
|
+
} },
|
|
229
|
+
messageTimestamp: now(),
|
|
230
|
+
}]);
|
|
231
|
+
|
|
232
|
+
const [loc, card] = received;
|
|
233
|
+
expect(loc.text.split("\n")).toHaveLength(1);
|
|
234
|
+
expect(loc.text).toContain("lat=1.5 lng=2");
|
|
235
|
+
expect(card.text).toContain("TEL;type=CELL:+1 555 0100");
|
|
236
|
+
expect(card.text).not.toContain("PHOTO");
|
|
237
|
+
expect(card.text.length).toBeLessThan(500);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
humanList,
|
|
4
|
+
normalizeUserJid,
|
|
5
|
+
pickContactChatJid,
|
|
6
|
+
pickRoutableJid,
|
|
7
|
+
resolveWhatsAppSender,
|
|
8
|
+
sanitizeMediaType,
|
|
9
|
+
unwrapMessage,
|
|
10
|
+
} from "./whatsapp";
|
|
11
|
+
import { whatsappChatLink } from "./whatsapp-link";
|
|
12
|
+
|
|
13
|
+
describe("whatsapp payload helpers", () => {
|
|
14
|
+
it("unwraps nested ephemeral and view-once envelopes", () => {
|
|
15
|
+
const inner = { imageMessage: { caption: "secret" } };
|
|
16
|
+
expect(unwrapMessage({ ephemeralMessage: { message: { viewOnceMessageV2: { message: inner } } } })).toBe(inner);
|
|
17
|
+
expect(unwrapMessage({ documentWithCaptionMessage: { message: { documentMessage: { caption: "c" } } } }))
|
|
18
|
+
.toEqual({ documentMessage: { caption: "c" } });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("stops unwrapping a pathologically deep envelope", () => {
|
|
22
|
+
let msg: Record<string, unknown> = { conversation: "deep" };
|
|
23
|
+
for (let i = 0; i < 50; i++) msg = { ephemeralMessage: { message: msg } };
|
|
24
|
+
expect(() => unwrapMessage(msg)).not.toThrow();
|
|
25
|
+
expect(unwrapMessage(msg)).not.toHaveProperty("conversation");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("only accepts allow-listed media types and strips parameters", () => {
|
|
29
|
+
expect(sanitizeMediaType("Image/JPEG; charset=binary", "image", "image/jpeg")).toBe("image/jpeg");
|
|
30
|
+
expect(sanitizeMediaType("text/html", "image", "image/jpeg")).toBe("image/jpeg");
|
|
31
|
+
expect(sanitizeMediaType("image/png\r\nX-Injected: 1", "image", "image/jpeg")).toBe("image/jpeg");
|
|
32
|
+
expect(sanitizeMediaType(undefined, "audio", "audio/ogg")).toBe("audio/ogg");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("prefers a routable alt JID over an @lid primary", () => {
|
|
36
|
+
expect(pickRoutableJid("123@lid", "15551234567@s.whatsapp.net")).toBe("15551234567@s.whatsapp.net");
|
|
37
|
+
expect(pickRoutableJid("family@g.us", "123@lid")).toBe("family@g.us");
|
|
38
|
+
expect(pickRoutableJid("123@lid", undefined)).toBe("123@lid");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keys contacts on the phone-number JID and rejects broadcasts", () => {
|
|
42
|
+
expect(pickContactChatJid({ id: "123@lid", phoneNumber: "15551234567@s.whatsapp.net" })).toBe("15551234567@s.whatsapp.net");
|
|
43
|
+
expect(pickContactChatJid({ id: "123@lid" })).toBe("123@lid");
|
|
44
|
+
expect(pickContactChatJid({ id: "status@broadcast" })).toBeNull();
|
|
45
|
+
expect(pickContactChatJid({})).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("strips device suffixes and legacy hosts from user JIDs", () => {
|
|
49
|
+
expect(normalizeUserJid("15551234567:23@s.whatsapp.net")).toBe("15551234567@s.whatsapp.net");
|
|
50
|
+
expect(normalizeUserJid("15551234567@c.us")).toBe("15551234567@s.whatsapp.net");
|
|
51
|
+
expect(normalizeUserJid(":5@s.whatsapp.net")).toBeNull();
|
|
52
|
+
expect(normalizeUserJid("no-at-sign")).toBeNull();
|
|
53
|
+
expect(normalizeUserJid(42)).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("summarizes long participant lists and skips blanks", () => {
|
|
57
|
+
expect(humanList(["a", " ", "b", "c"], 2)).toBe("a, b, and 1 more");
|
|
58
|
+
expect(humanList(["", " "], 3)).toBe("(none)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("links only the chats a routed message can actually be opened in", () => {
|
|
62
|
+
const routed = pickRoutableJid("123@lid", "15551234567@s.whatsapp.net");
|
|
63
|
+
expect(whatsappChatLink(routed)).toBe("https://wa.me/15551234567");
|
|
64
|
+
expect(whatsappChatLink(pickRoutableJid("123@lid", undefined))).toBeNull();
|
|
65
|
+
expect(whatsappChatLink(normalizeUserJid("15551234567:7@s.whatsapp.net"))).toBe("https://wa.me/15551234567");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
3
68
|
|
|
4
69
|
describe("resolveWhatsAppSender", () => {
|
|
5
70
|
it("attributes user-authored DM replies to the paired account", () => {
|
|
@@ -49,4 +114,18 @@ describe("resolveWhatsAppSender", () => {
|
|
|
49
114
|
senderName: "Robert",
|
|
50
115
|
});
|
|
51
116
|
});
|
|
117
|
+
|
|
118
|
+
it("falls back past blank display names", () => {
|
|
119
|
+
const base = {
|
|
120
|
+
fromMe: false,
|
|
121
|
+
selfJid: "me@s.whatsapp.net",
|
|
122
|
+
participantName: null,
|
|
123
|
+
};
|
|
124
|
+
expect(resolveWhatsAppSender({ ...base, isGroup: false, remoteJid: "c@s.whatsapp.net", participantJid: null, pushName: "", chatName: " Carol " }))
|
|
125
|
+
.toEqual({ senderJid: "c@s.whatsapp.net", senderName: "Carol" });
|
|
126
|
+
expect(resolveWhatsAppSender({ ...base, isGroup: false, remoteJid: "c@s.whatsapp.net", participantJid: null, pushName: " ", chatName: null }))
|
|
127
|
+
.toEqual({ senderJid: "c@s.whatsapp.net", senderName: "c@s.whatsapp.net" });
|
|
128
|
+
expect(resolveWhatsAppSender({ ...base, isGroup: true, remoteJid: "g@g.us", participantJid: null, pushName: "", chatName: "G", participantName: "" }))
|
|
129
|
+
.toEqual({ senderJid: "g@g.us", senderName: "g@g.us" });
|
|
130
|
+
});
|
|
52
131
|
});
|
package/lib/bridges/whatsapp.ts
CHANGED
|
@@ -29,12 +29,13 @@ import {
|
|
|
29
29
|
bumpRouteLastSeenTs,
|
|
30
30
|
findRoute,
|
|
31
31
|
getBridge,
|
|
32
|
-
|
|
32
|
+
getRouteLastSeenTsMap,
|
|
33
33
|
} from "@/lib/stores/bridges";
|
|
34
34
|
import { ensureWhatsAppAuthDir, removeWhatsAppAuth } from "./modules/whatsapp/auth-state";
|
|
35
35
|
import type { BridgeAdapter, ChatInfo, InboundEvent, InboundHandler, StatusHandler, InboundMessage, StatusUpdate } from "./types";
|
|
36
36
|
import type { ContentPart } from "@/lib/tools/runtime/types";
|
|
37
37
|
import { storeCanonicalBridgeAttachment } from "./attachment-store";
|
|
38
|
+
import { whatsappChatLink } from "./whatsapp-link";
|
|
38
39
|
import { errorMessage } from "@/lib/utils/error";
|
|
39
40
|
|
|
40
41
|
// Baileys + qrcode are dev-time-installed peer libs. We never import their
|
|
@@ -285,12 +286,12 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
285
286
|
// adapter should re-deliver to agents) and the legacy
|
|
286
287
|
// `replace`/`update` events that arrive via messages.update.
|
|
287
288
|
if (payload.type !== "notify" && payload.type !== "append") return;
|
|
288
|
-
// On a fresh adapter boot the
|
|
289
|
-
// persisted. Read
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
?
|
|
293
|
-
:
|
|
289
|
+
// On a fresh adapter boot the watermarks are whatever the last run
|
|
290
|
+
// persisted. Read them once per batch (one cheap SQLite call) so old
|
|
291
|
+
// appends are rejected without per-message queries.
|
|
292
|
+
const watermarks = payload.type === "append"
|
|
293
|
+
? getRouteLastSeenTsMap(this.bridge_id)
|
|
294
|
+
: null;
|
|
294
295
|
for (const raw of payload.messages ?? []) {
|
|
295
296
|
const m = raw as {
|
|
296
297
|
key?: { remoteJid?: string; remoteJidAlt?: string; fromMe?: boolean; id?: string; participant?: string };
|
|
@@ -324,18 +325,20 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
324
325
|
const ts = typeof m.messageTimestamp === "number"
|
|
325
326
|
? m.messageTimestamp * 1000
|
|
326
327
|
: (m.messageTimestamp?.low ?? 0) * 1000 || Date.now();
|
|
327
|
-
// Watermark gate: on `append` batches, skip anything
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
if (
|
|
328
|
+
// Watermark gate: on `append` batches, skip anything at or below
|
|
329
|
+
// this chat's own persisted watermark. This prevents the server's
|
|
330
|
+
// reconnect replay from re-delivering messages we already routed in
|
|
331
|
+
// a previous process lifetime. `notify` events skip this check —
|
|
332
|
+
// they're live and may legitimately arrive out of order.
|
|
333
|
+
const watermark = watermarks?.get(remote_jid) ?? 0;
|
|
334
|
+
if (watermark > 0 && ts > 0 && ts <= watermark) {
|
|
334
335
|
continue;
|
|
335
336
|
}
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
337
|
+
// pushName names the *sender*: in groups that's a participant, and on
|
|
338
|
+
// fromMe messages it's the user's own name. Only a counterpart's
|
|
339
|
+
// pushName in a 1:1 chat is a valid label for the chat itself.
|
|
340
|
+
const chatPushName = is_group || m.key.fromMe ? null : (m.pushName ?? null);
|
|
341
|
+
this.observeChat(remote_jid, chatPushName, ts);
|
|
339
342
|
|
|
340
343
|
// Unwrap protocol envelopes (view-once, ephemeral, etc.) so we can
|
|
341
344
|
// see the underlying payload uniformly, then turn it into a text
|
|
@@ -357,7 +360,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
357
360
|
const participant_jid = is_group && m.key.participant
|
|
358
361
|
? normalizeUserJid(m.key.participant)
|
|
359
362
|
: null;
|
|
360
|
-
const chat_name = this.chats.get(remote_jid)?.name ??
|
|
363
|
+
const chat_name = this.chats.get(remote_jid)?.name ?? chatPushName;
|
|
361
364
|
const sender = resolveWhatsAppSender({
|
|
362
365
|
fromMe: m.key.fromMe === true,
|
|
363
366
|
isGroup: is_group,
|
|
@@ -422,7 +425,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
422
425
|
};
|
|
423
426
|
const chatJid = pickContactChatJid(ct);
|
|
424
427
|
if (!chatJid) continue;
|
|
425
|
-
const name = ct.name
|
|
428
|
+
const name = [ct.name, ct.verifiedName, ct.notify].find((n) => n?.trim()) ?? null;
|
|
426
429
|
this.observeChat(chatJid, name, null);
|
|
427
430
|
}
|
|
428
431
|
});
|
|
@@ -457,7 +460,7 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
457
460
|
for (const ct of list) {
|
|
458
461
|
const chatJid = pickContactChatJid(ct);
|
|
459
462
|
if (!chatJid) continue;
|
|
460
|
-
this.observeChat(chatJid, ct.name
|
|
463
|
+
this.observeChat(chatJid, [ct.name, ct.verifiedName, ct.notify].find((n) => n?.trim()) ?? null, null);
|
|
461
464
|
}
|
|
462
465
|
});
|
|
463
466
|
|
|
@@ -592,6 +595,10 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
592
595
|
this.pushStatus({ status: "disconnected", qr_data_url: null, error: null });
|
|
593
596
|
}
|
|
594
597
|
|
|
598
|
+
chatLink(remote_jid: string): string | null {
|
|
599
|
+
return whatsappChatLink(remote_jid);
|
|
600
|
+
}
|
|
601
|
+
|
|
595
602
|
async sendText(remote_jid: string, text: string): Promise<void> {
|
|
596
603
|
// Hard guard: refuse to send to any chat whose route is in silent_mode.
|
|
597
604
|
// The dispatcher already short-circuits before calling sendText, but
|
|
@@ -912,8 +919,11 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
912
919
|
});
|
|
913
920
|
attachments.push(ref);
|
|
914
921
|
const sizeKb = Math.max(1, Math.round(buf.length / 1024));
|
|
922
|
+
// Sender-supplied document names can carry newlines or brackets;
|
|
923
|
+
// describe the attachment by the store's sanitized name.
|
|
924
|
+
const shownName = oneLine(("filename" in ref && ref.filename) || filename);
|
|
915
925
|
text = (text ? text + "\n" : "")
|
|
916
|
-
+ `[Attached ${label}: ${
|
|
926
|
+
+ `[Attached ${label}: ${shownName} (${mime}, ${sizeKb} KB); retained as a Jarela file reference.]`;
|
|
917
927
|
} catch (err) {
|
|
918
928
|
const m = errorMessage(err);
|
|
919
929
|
console.warn(`[bridge ${this.bridge_id}] failed to spill ${label} from ${remote_jid}: ${m}`);
|
|
@@ -975,8 +985,8 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
975
985
|
if (inner.locationMessage) {
|
|
976
986
|
const { degreesLatitude, degreesLongitude, name, address } = inner.locationMessage;
|
|
977
987
|
const parts = [`lat=${degreesLatitude ?? "?"}`, `lng=${degreesLongitude ?? "?"}`];
|
|
978
|
-
if (name) parts.push(`name
|
|
979
|
-
if (address) parts.push(`address
|
|
988
|
+
if (name) parts.push(`name=${JSON.stringify(oneLine(name))}`);
|
|
989
|
+
if (address) parts.push(`address=${JSON.stringify(oneLine(address))}`);
|
|
980
990
|
text = (text ? text + "\n" : "") + `[location: ${parts.join(" ")}]`;
|
|
981
991
|
}
|
|
982
992
|
|
|
@@ -986,14 +996,14 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
986
996
|
}
|
|
987
997
|
|
|
988
998
|
if (inner.contactMessage) {
|
|
989
|
-
const name = inner.contactMessage.displayName ?? "unknown";
|
|
990
|
-
const vcard = inner.contactMessage.vcard ?? "";
|
|
999
|
+
const name = oneLine(inner.contactMessage.displayName ?? "") || "unknown";
|
|
1000
|
+
const vcard = stripVcardBinary(inner.contactMessage.vcard ?? "");
|
|
991
1001
|
text = (text ? text + "\n" : "") + `[contact: ${name}]\n${vcard}`;
|
|
992
1002
|
}
|
|
993
1003
|
|
|
994
1004
|
if (inner.contactsArrayMessage) {
|
|
995
1005
|
const names = (inner.contactsArrayMessage.contacts ?? [])
|
|
996
|
-
.map((c) => c?.displayName ?? "unknown")
|
|
1006
|
+
.map((c) => oneLine(c?.displayName ?? "") || "unknown")
|
|
997
1007
|
.join(", ");
|
|
998
1008
|
text = (text ? text + "\n" : "") + `[contacts: ${names}]`;
|
|
999
1009
|
}
|
|
@@ -1005,8 +1015,9 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
1005
1015
|
if (!remote_jid) return;
|
|
1006
1016
|
const existing = this.chats.get(remote_jid);
|
|
1007
1017
|
// Prefer the most recently-observed name (group subjects rename; users
|
|
1008
|
-
// change push names). Don't overwrite a known name with
|
|
1009
|
-
|
|
1018
|
+
// change push names). Don't overwrite a known name with a missing or
|
|
1019
|
+
// blank one.
|
|
1020
|
+
const nextName = name?.trim() || existing?.name || null;
|
|
1010
1021
|
const nextTs = ts && (!existing?.last_message_at || ts > existing.last_message_at)
|
|
1011
1022
|
? ts
|
|
1012
1023
|
: existing?.last_message_at ?? null;
|
|
@@ -1026,6 +1037,30 @@ export class WhatsAppBridgeAdapter implements BridgeAdapter {
|
|
|
1026
1037
|
get status(): StatusUpdate["status"] { return this.currentStatus; }
|
|
1027
1038
|
}
|
|
1028
1039
|
|
|
1040
|
+
export function oneLine(value: string): string {
|
|
1041
|
+
return value.replace(/[\s\u0085\u2028\u2029]+/g, " ").trim();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Phones embed contact photos and logos as base64 vCard properties, often
|
|
1046
|
+
* tens of KB of noise in the agent's context. Drop those properties,
|
|
1047
|
+
* including their folded continuation lines (RFC 6350 §3.2), and keep the
|
|
1048
|
+
* readable fields.
|
|
1049
|
+
*/
|
|
1050
|
+
export function stripVcardBinary(vcard: string): string {
|
|
1051
|
+
const out: string[] = [];
|
|
1052
|
+
let skipping = false;
|
|
1053
|
+
for (const line of vcard.split(/\r\n|\r|\n/)) {
|
|
1054
|
+
if (/^[ \t]/.test(line)) {
|
|
1055
|
+
if (!skipping) out.push(line);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
skipping = /^(?:[\w-]+\.)?(?:PHOTO|LOGO|SOUND|KEY)[;:]/i.test(line);
|
|
1059
|
+
if (!skipping) out.push(line);
|
|
1060
|
+
}
|
|
1061
|
+
return out.join("\n").trim();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1029
1064
|
// Minimal pino-compatible silent logger. Baileys uses `logger.child()`
|
|
1030
1065
|
// chaining internally, so we return ourselves for every child call.
|
|
1031
1066
|
function makeSilentLogger(): unknown {
|
|
@@ -1070,7 +1105,7 @@ const MEDIA_TYPE_ALLOWLIST: Record<"image" | "audio" | "video" | "document", Rea
|
|
|
1070
1105
|
]),
|
|
1071
1106
|
};
|
|
1072
1107
|
|
|
1073
|
-
function sanitizeMediaType(
|
|
1108
|
+
export function sanitizeMediaType(
|
|
1074
1109
|
raw: string | undefined,
|
|
1075
1110
|
kind: "image" | "audio" | "video" | "document",
|
|
1076
1111
|
fallback: string,
|
|
@@ -1088,7 +1123,7 @@ function sanitizeMediaType(
|
|
|
1088
1123
|
* in several envelope variants (view-once, ephemeral, V2 versions); see
|
|
1089
1124
|
* `unwrapMessage` for how we collapse them.
|
|
1090
1125
|
*/
|
|
1091
|
-
type WAMessageContent = {
|
|
1126
|
+
export type WAMessageContent = {
|
|
1092
1127
|
conversation?: string;
|
|
1093
1128
|
extendedTextMessage?: { text?: string };
|
|
1094
1129
|
stickerMessage?: { mimetype?: string; isAnimated?: boolean };
|
|
@@ -1113,7 +1148,7 @@ type WAMessageContent = {
|
|
|
1113
1148
|
* payload. Bounded recursion — these envelopes never nest deeper than 2
|
|
1114
1149
|
* levels in practice, but we cap at 5 as defence in depth.
|
|
1115
1150
|
*/
|
|
1116
|
-
function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageContent | undefined {
|
|
1151
|
+
export function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageContent | undefined {
|
|
1117
1152
|
if (!msg || depth > 5) return msg;
|
|
1118
1153
|
if (msg.ephemeralMessage?.message) return unwrapMessage(msg.ephemeralMessage.message, depth + 1);
|
|
1119
1154
|
if (msg.viewOnceMessage?.message) return unwrapMessage(msg.viewOnceMessage.message, depth + 1);
|
|
@@ -1128,7 +1163,7 @@ function unwrapMessage(msg: WAMessageContent | undefined, depth = 0): WAMessageC
|
|
|
1128
1163
|
* country-code + number with no separators, no leading '+'. Accepts the
|
|
1129
1164
|
* common human formats: "+1 (555) 123-4567", "5511 99999-0000", etc.
|
|
1130
1165
|
*/
|
|
1131
|
-
function normalizePhoneDigits(input: string): string {
|
|
1166
|
+
export function normalizePhoneDigits(input: string): string {
|
|
1132
1167
|
return input.replace(/\D+/g, "");
|
|
1133
1168
|
}
|
|
1134
1169
|
|
|
@@ -1142,7 +1177,7 @@ function normalizePhoneDigits(input: string): string {
|
|
|
1142
1177
|
* the `@s.whatsapp.net` form — without this normalization, every inbound
|
|
1143
1178
|
* `@lid` message would silently fail to match its route.
|
|
1144
1179
|
*/
|
|
1145
|
-
function pickRoutableJid(primary: string, alt: string | undefined): string {
|
|
1180
|
+
export function pickRoutableJid(primary: string, alt: string | undefined): string {
|
|
1146
1181
|
const isRoutable = (j: string | undefined) =>
|
|
1147
1182
|
!!j && (j.endsWith("@s.whatsapp.net") || j.endsWith("@g.us"));
|
|
1148
1183
|
if (isRoutable(primary)) return primary;
|
|
@@ -1160,6 +1195,9 @@ export function resolveWhatsAppSender(input: {
|
|
|
1160
1195
|
chatName: string | null;
|
|
1161
1196
|
participantName: string | null;
|
|
1162
1197
|
}): { senderJid: string; senderName: string } {
|
|
1198
|
+
// Platforms send blank display names ("" or whitespace) for contacts that
|
|
1199
|
+
// never set one; treat those as missing so the fallbacks still apply.
|
|
1200
|
+
const named = (...names: Array<string | null>) => names.find((n) => !!n && n.trim().length > 0)?.trim();
|
|
1163
1201
|
if (input.fromMe) {
|
|
1164
1202
|
return {
|
|
1165
1203
|
senderJid: input.selfJid ?? "me",
|
|
@@ -1170,12 +1208,12 @@ export function resolveWhatsAppSender(input: {
|
|
|
1170
1208
|
const senderJid = input.participantJid ?? input.remoteJid;
|
|
1171
1209
|
return {
|
|
1172
1210
|
senderJid,
|
|
1173
|
-
senderName: input.participantName
|
|
1211
|
+
senderName: named(input.participantName, input.pushName) ?? senderJid,
|
|
1174
1212
|
};
|
|
1175
1213
|
}
|
|
1176
1214
|
return {
|
|
1177
1215
|
senderJid: input.remoteJid,
|
|
1178
|
-
senderName: input.pushName
|
|
1216
|
+
senderName: named(input.pushName, input.chatName) ?? input.remoteJid,
|
|
1179
1217
|
};
|
|
1180
1218
|
}
|
|
1181
1219
|
|
|
@@ -1198,7 +1236,7 @@ export function resolveWhatsAppSender(input: {
|
|
|
1198
1236
|
* Returns null for JIDs that aren't routable chats (self broadcast,
|
|
1199
1237
|
* status@broadcast, malformed ids, empty input).
|
|
1200
1238
|
*/
|
|
1201
|
-
function pickContactChatJid(ct: {
|
|
1239
|
+
export function pickContactChatJid(ct: {
|
|
1202
1240
|
id?: string;
|
|
1203
1241
|
phoneNumber?: string;
|
|
1204
1242
|
lid?: string;
|
|
@@ -1220,7 +1258,7 @@ function pickContactChatJid(ct: {
|
|
|
1220
1258
|
* routable user JID — `sendMessage` won't deliver to a JID with `:NN`.
|
|
1221
1259
|
* Returns null if the input is malformed.
|
|
1222
1260
|
*/
|
|
1223
|
-
function normalizeUserJid(id: unknown): string | null {
|
|
1261
|
+
export function normalizeUserJid(id: unknown): string | null {
|
|
1224
1262
|
if (typeof id !== "string") return null;
|
|
1225
1263
|
// Strip any device suffix (":NN") before the '@'.
|
|
1226
1264
|
const at = id.indexOf("@");
|
|
@@ -1234,7 +1272,7 @@ function normalizeUserJid(id: unknown): string | null {
|
|
|
1234
1272
|
return `${user}@${normHost}`;
|
|
1235
1273
|
}
|
|
1236
1274
|
|
|
1237
|
-
function humanList(items: string[], max: number): string {
|
|
1275
|
+
export function humanList(items: string[], max: number): string {
|
|
1238
1276
|
const clean = items.filter((s) => s && s.trim().length > 0);
|
|
1239
1277
|
if (clean.length === 0) return "(none)";
|
|
1240
1278
|
if (clean.length <= max) return clean.join(", ");
|