@rubytech/create-sitedesk-code 0.1.518 → 0.1.519
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/dist/__tests__/edge-bind-template-wiring.test.js +37 -0
- package/dist/__tests__/samba-provision.test.js +103 -1
- package/dist/index.js +19 -4
- package/dist/samba-provision.js +72 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +69 -19
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +43 -5
- package/payload/platform/plugins/docs/references/samba.md +2 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +7 -3
- package/payload/platform/scripts/check-no-esm-require.mjs +5 -3
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +5 -3
- package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.js +13 -10
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts +6 -2
- package/payload/platform/services/webchat-channel/dist/instructions.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +66 -32
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +11 -4
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +30 -14
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/turn-follow.js +11 -5
- package/payload/platform/services/webchat-channel/dist/turn-follow.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -3
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +9 -5
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +6 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +8 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/templates/systemd/edge.service.template +1 -1
- package/payload/server/chunk-PXNY2TAP.js +1980 -0
- package/payload/server/chunk-TIIWMF5G.js +4837 -0
- package/payload/server/{chunk-S6HYTIU3.js → chunk-UX2FRNO7.js} +72 -2031
- package/payload/server/manager-B6OFOXSW.js +61 -0
- package/payload/server/maxy-edge.js +5 -3
- package/payload/server/package.json +2 -1
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-CDpNkwkf.js → AdminLoginScreens-BZPbCk8g.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-BZPbCk8g.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js +2 -0
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.br +0 -0
- package/payload/server/public/assets/AdminShell-Bpo_wgxm.js.gz +0 -0
- package/payload/server/public/assets/{activity-BE9gv5xL.js → activity-DOsfKzT3.js} +1 -1
- package/payload/server/public/assets/activity-DOsfKzT3.js.br +0 -0
- package/payload/server/public/assets/activity-DOsfKzT3.js.gz +0 -0
- package/payload/server/public/assets/admin-COgD1z5a.js +1 -0
- package/payload/server/public/assets/admin-COgD1z5a.js.br +0 -0
- package/payload/server/public/assets/admin-COgD1z5a.js.gz +0 -0
- package/payload/server/public/assets/{agents-B-x0Ai2v.js → agents-DqDnZTH_.js} +1 -1
- package/payload/server/public/assets/agents-DqDnZTH_.js.br +0 -0
- package/payload/server/public/assets/agents-DqDnZTH_.js.gz +0 -0
- package/payload/server/public/assets/{browser-ZEQUkPfs.js → browser-FL17Dfp0.js} +1 -1
- package/payload/server/public/assets/browser-FL17Dfp0.js.br +0 -0
- package/payload/server/public/assets/browser-FL17Dfp0.js.gz +0 -0
- package/payload/server/public/assets/calendar-CLfLvsnV.js +1 -0
- package/payload/server/public/assets/calendar-CLfLvsnV.js.br +0 -0
- package/payload/server/public/assets/calendar-CLfLvsnV.js.gz +0 -0
- package/payload/server/public/assets/chat-DYq4FxMG.js +1 -0
- package/payload/server/public/assets/chat-DYq4FxMG.js.br +0 -0
- package/payload/server/public/assets/chat-DYq4FxMG.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DWt5cdUo.js +1 -0
- package/payload/server/public/assets/chevron-right-B9suVrYy.js +1 -0
- package/payload/server/public/assets/chevron-right-B9suVrYy.js.br +0 -0
- package/payload/server/public/assets/clock-DW6qVXtv.js +1 -0
- package/payload/server/public/assets/clock-DW6qVXtv.js.br +0 -0
- package/payload/server/public/assets/clock-DW6qVXtv.js.gz +0 -0
- package/payload/server/public/assets/data-C8rbOgds.js +1 -0
- package/payload/server/public/assets/data-C8rbOgds.js.br +0 -0
- package/payload/server/public/assets/data-C8rbOgds.js.gz +0 -0
- package/payload/server/public/assets/{file-text-BTV6EdBv.js → file-text-DNm0AzUz.js} +1 -1
- package/payload/server/public/assets/file-text-DNm0AzUz.js.br +0 -0
- package/payload/server/public/assets/file-text-DNm0AzUz.js.gz +0 -0
- package/payload/server/public/assets/{graph-hD_gPbbL.js → graph-fAvmlVzh.js} +3 -3
- package/payload/server/public/assets/graph-fAvmlVzh.js.br +0 -0
- package/payload/server/public/assets/graph-fAvmlVzh.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-CY1T19PY.js → graph-labels-CwdkCxfs.js} +1 -1
- package/payload/server/public/assets/graph-labels-CwdkCxfs.js.br +0 -0
- package/payload/server/public/assets/graph-labels-CwdkCxfs.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-BFV9VKHO.js → maximize-2-B6tAPhC2.js} +1 -1
- package/payload/server/public/assets/maximize-2-B6tAPhC2.js.br +0 -0
- package/payload/server/public/assets/maximize-2-B6tAPhC2.js.gz +0 -0
- package/payload/server/public/assets/operator-CTKvBVjk.js +1 -0
- package/payload/server/public/assets/operator-CTKvBVjk.js.br +0 -0
- package/payload/server/public/assets/operator-CTKvBVjk.js.gz +0 -0
- package/payload/server/public/assets/page-BWoNW3sa.js +32 -0
- package/payload/server/public/assets/page-BWoNW3sa.js.br +0 -0
- package/payload/server/public/assets/page-BWoNW3sa.js.gz +0 -0
- package/payload/server/public/assets/page-O5HkHQw4.js +1 -0
- package/payload/server/public/assets/page-O5HkHQw4.js.br +0 -0
- package/payload/server/public/assets/page-O5HkHQw4.js.gz +0 -0
- package/payload/server/public/assets/{public-D6Tnj5LU.js → public-BF1rnVPa.js} +1 -1
- package/payload/server/public/assets/public-BF1rnVPa.js.br +0 -0
- package/payload/server/public/assets/public-BF1rnVPa.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-C-RfQUza.js → rotate-ccw-B8TqfCy4.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-B8TqfCy4.js.gz +0 -0
- package/payload/server/public/assets/{routines-NIMElUzV.js → routines-4R7BLZyH.js} +1 -1
- package/payload/server/public/assets/routines-4R7BLZyH.js.br +0 -0
- package/payload/server/public/assets/routines-4R7BLZyH.js.gz +0 -0
- package/payload/server/public/assets/{skills-N0ZlHosx.js → skills-CQ9Qs2tg.js} +1 -1
- package/payload/server/public/assets/skills-CQ9Qs2tg.js.br +0 -0
- package/payload/server/public/assets/skills-CQ9Qs2tg.js.gz +0 -0
- package/payload/server/public/assets/tasks-DePyOrRk.js +1 -0
- package/payload/server/public/assets/tasks-DePyOrRk.js.br +0 -0
- package/payload/server/public/assets/tasks-DePyOrRk.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-jQxlRPoI.js → time-entry-format-D9gtnG01.js} +1 -1
- package/payload/server/public/assets/time-entry-format-D9gtnG01.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-D9gtnG01.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-wUS4rpzv.js → triangle-alert-DAtm2mLL.js} +1 -1
- package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-DAtm2mLL.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BRix_wIl.js → useCopyFeedback-uMJT-p6i.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-uMJT-p6i.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js +14 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BufCPDDH.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-iCzsV96x.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js +2 -0
- package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BFrPC-s7.js.gz +0 -0
- package/payload/server/public/assets/{wrench-sDivAjhf.js → wrench-CYgaN7K9.js} +1 -1
- package/payload/server/public/assets/wrench-CYgaN7K9.js.br +1 -0
- package/payload/server/public/assets/wrench-CYgaN7K9.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +6100 -9940
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CDpNkwkf.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js +0 -2
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.br +0 -0
- package/payload/server/public/assets/AdminShell-TSa8KgID.js.gz +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.br +0 -0
- package/payload/server/public/assets/activity-BE9gv5xL.js.gz +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js +0 -1
- package/payload/server/public/assets/admin-Cb0BZriv.js.br +0 -0
- package/payload/server/public/assets/admin-Cb0BZriv.js.gz +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.br +0 -0
- package/payload/server/public/assets/agents-B-x0Ai2v.js.gz +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.br +0 -0
- package/payload/server/public/assets/browser-ZEQUkPfs.js.gz +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js +0 -1
- package/payload/server/public/assets/calendar-DsRxrW0G.js.br +0 -0
- package/payload/server/public/assets/calendar-DsRxrW0G.js.gz +0 -0
- package/payload/server/public/assets/chat-BS9B9lDo.js +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.br +0 -1
- package/payload/server/public/assets/chat-BS9B9lDo.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DLtGuH6K.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js +0 -1
- package/payload/server/public/assets/chevron-right-CY_trg5L.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js +0 -1
- package/payload/server/public/assets/clock-oM5driJm.js.br +0 -0
- package/payload/server/public/assets/clock-oM5driJm.js.gz +0 -0
- package/payload/server/public/assets/data-Cio6PO0F.js +0 -1
- package/payload/server/public/assets/data-Cio6PO0F.js.br +0 -2
- package/payload/server/public/assets/data-Cio6PO0F.js.gz +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.br +0 -0
- package/payload/server/public/assets/file-text-BTV6EdBv.js.gz +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.br +0 -0
- package/payload/server/public/assets/graph-hD_gPbbL.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.br +0 -0
- package/payload/server/public/assets/graph-labels-CY1T19PY.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.br +0 -0
- package/payload/server/public/assets/maximize-2-BFV9VKHO.js.gz +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js +0 -1
- package/payload/server/public/assets/operator-Dxze8CwV.js.br +0 -0
- package/payload/server/public/assets/operator-Dxze8CwV.js.gz +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js +0 -32
- package/payload/server/public/assets/page-Bi86QF6_.js.br +0 -0
- package/payload/server/public/assets/page-Bi86QF6_.js.gz +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js +0 -1
- package/payload/server/public/assets/page-BnXly9aN.js.br +0 -0
- package/payload/server/public/assets/page-BnXly9aN.js.gz +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.br +0 -0
- package/payload/server/public/assets/public-D6Tnj5LU.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-C-RfQUza.js.gz +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.br +0 -0
- package/payload/server/public/assets/routines-NIMElUzV.js.gz +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.br +0 -0
- package/payload/server/public/assets/skills-N0ZlHosx.js.gz +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js +0 -1
- package/payload/server/public/assets/tasks-BsB_0duv.js.br +0 -0
- package/payload/server/public/assets/tasks-BsB_0duv.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-jQxlRPoI.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-wUS4rpzv.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BRix_wIl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js +0 -14
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-DWMWxvYo.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js +0 -2
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-1TAfCP2k.js.gz +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.br +0 -0
- package/payload/server/public/assets/wrench-sDivAjhf.js.gz +0 -0
|
@@ -0,0 +1,4837 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
ACCOUNTS_DIR,
|
|
4
|
+
LID_MAP_FILE,
|
|
5
|
+
MAXY_DIR,
|
|
6
|
+
ensureConversation,
|
|
7
|
+
getSession,
|
|
8
|
+
getUserTimezone,
|
|
9
|
+
isValidAccountId
|
|
10
|
+
} from "./chunk-PXNY2TAP.js";
|
|
11
|
+
|
|
12
|
+
// app/lib/whatsapp/schema.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
15
|
+
var WhatsAppAccountSchema = z.object({
|
|
16
|
+
name: z.string().optional().describe("Display name for this WhatsApp account (e.g. 'Main', 'Sales')."),
|
|
17
|
+
enabled: z.boolean().optional().describe("If disabled, this account won't connect on startup."),
|
|
18
|
+
dmPolicy: DmPolicySchema.optional().describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
|
|
19
|
+
selfChatMode: z.boolean().optional().describe("Enable if the bot uses the owner's personal WhatsApp number (same phone for admin and bot). Changes how self-messages are routed."),
|
|
20
|
+
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
|
|
21
|
+
allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
|
|
22
|
+
sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
|
|
23
|
+
ackReaction: z.object({
|
|
24
|
+
emoji: z.string().optional(),
|
|
25
|
+
direct: z.boolean().optional().default(true),
|
|
26
|
+
group: z.enum(["always", "mentions", "never"]).optional().default("mentions")
|
|
27
|
+
}).strict().optional().describe("React to incoming messages with an emoji to acknowledge receipt before the agent responds."),
|
|
28
|
+
debounceMs: z.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
|
|
29
|
+
publicAgent: z.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set."),
|
|
30
|
+
afterHoursMessage: z.string().optional().describe("Auto-reply text sent to public senders when the business is closed (outside opening hours). If not set, messages received outside business hours are silently skipped with no reply."),
|
|
31
|
+
dispatchInbound: z.boolean().optional().describe("When false, this account's WhatsApp socket only observes: inbound is stored and readable but never dispatched to an agent. Default true. Set with set-dispatch-inbound.")
|
|
32
|
+
}).strict().superRefine((value, ctx) => {
|
|
33
|
+
if (value.dmPolicy !== "open") return;
|
|
34
|
+
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
35
|
+
if (allow.includes("*")) return;
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["allowFrom"],
|
|
39
|
+
message: 'dmPolicy="open" requires allowFrom to include "*"'
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
var WhatsAppConfigSchema = z.object({
|
|
43
|
+
accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
|
|
44
|
+
dmPolicy: DmPolicySchema.optional().default("disabled").describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
|
|
45
|
+
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone. Authoritative only on the account that owns the WhatsApp paired socket; a list on any other account is inert and purged at boot \u2014 use accountManagers for sub-account routing."),
|
|
46
|
+
accountManagers: z.record(
|
|
47
|
+
z.string(),
|
|
48
|
+
z.union([
|
|
49
|
+
z.string(),
|
|
50
|
+
z.object({ managesAccount: z.string(), mode: z.enum(["active", "passive"]).optional() })
|
|
51
|
+
])
|
|
52
|
+
).optional().describe("Account-manager bindings: E.164 phone \u2192 sub-account. The value is a bare sub-account UUID (mode 'active', the default: a full admin session) or { managesAccount, mode } where mode 'passive' selects a constrained requirement-intake spawn (only work-create, no reply). A bare string and an object without mode both resolve to 'active'. Managed via add-account-manager / remove-account-manager. A phone here must not also be in adminPhones. Read only from the account whose account.json the manager loads its config from (the house); add-account-manager is refused on any other account, and a map left there is inert and purged at boot."),
|
|
53
|
+
allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
|
|
54
|
+
sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
|
|
55
|
+
mediaMaxMb: z.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
|
|
56
|
+
textChunkLimit: z.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
|
|
57
|
+
debounceMs: z.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
|
|
58
|
+
publicAgent: z.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones. Per-account overrides take precedence."),
|
|
59
|
+
afterHoursMessage: z.string().optional().describe("Default auto-reply text sent to public senders when the business is closed (outside opening hours). Per-account overrides take precedence. If not set, messages received outside business hours are silently skipped.")
|
|
60
|
+
}).strict().superRefine((value, ctx) => {
|
|
61
|
+
if (value.dmPolicy !== "open") return;
|
|
62
|
+
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
63
|
+
if (allow.includes("*")) return;
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
path: ["allowFrom"],
|
|
67
|
+
message: 'dmPolicy="open" requires allowFrom to include "*"'
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
function parseWhatsAppConfigTolerant(raw) {
|
|
71
|
+
const first = WhatsAppConfigSchema.safeParse(raw);
|
|
72
|
+
if (first.success) {
|
|
73
|
+
return { ok: true, data: first.data, droppedKeys: [] };
|
|
74
|
+
}
|
|
75
|
+
const issues = first.error.issues;
|
|
76
|
+
const onlyUnknownKeys = issues.length > 0 && issues.every((i) => i.code === "unrecognized_keys");
|
|
77
|
+
if (!onlyUnknownKeys) {
|
|
78
|
+
return { ok: false, error: first.error };
|
|
79
|
+
}
|
|
80
|
+
const clone = JSON.parse(JSON.stringify(raw));
|
|
81
|
+
const droppedKeys = [];
|
|
82
|
+
for (const issue of issues) {
|
|
83
|
+
if (issue.code !== "unrecognized_keys") continue;
|
|
84
|
+
const container = issue.path.reduce(
|
|
85
|
+
(acc, seg) => acc && typeof acc === "object" ? acc[seg] : void 0,
|
|
86
|
+
clone
|
|
87
|
+
);
|
|
88
|
+
if (!container || typeof container !== "object") continue;
|
|
89
|
+
for (const key of issue.keys) {
|
|
90
|
+
delete container[key];
|
|
91
|
+
droppedKeys.push([...issue.path, key].join("."));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const second = WhatsAppConfigSchema.safeParse(clone);
|
|
95
|
+
if (second.success) {
|
|
96
|
+
return { ok: true, data: second.data, droppedKeys };
|
|
97
|
+
}
|
|
98
|
+
return { ok: false, error: second.error };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// app/lib/whatsapp/auth-store.ts
|
|
102
|
+
import fsSync from "fs";
|
|
103
|
+
import fs from "fs/promises";
|
|
104
|
+
import path from "path";
|
|
105
|
+
import os from "os";
|
|
106
|
+
var TAG = "[whatsapp:auth]";
|
|
107
|
+
function resolveAuthDir(configDir2, accountId) {
|
|
108
|
+
return path.join(os.homedir(), configDir2, "credentials", "whatsapp", accountId);
|
|
109
|
+
}
|
|
110
|
+
function resolveCredsPath(authDir) {
|
|
111
|
+
return path.join(authDir, "creds.json");
|
|
112
|
+
}
|
|
113
|
+
function resolveCredsBackupPath(authDir) {
|
|
114
|
+
return path.join(authDir, "creds.json.bak");
|
|
115
|
+
}
|
|
116
|
+
function hasCredsSync(authDir) {
|
|
117
|
+
try {
|
|
118
|
+
const stats = fsSync.statSync(resolveCredsPath(authDir));
|
|
119
|
+
return stats.isFile() && stats.size > 1;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function authExists(authDir) {
|
|
125
|
+
const credsPath = resolveCredsPath(authDir);
|
|
126
|
+
try {
|
|
127
|
+
await fs.access(authDir);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const stats = await fs.stat(credsPath);
|
|
133
|
+
if (!stats.isFile() || stats.size <= 1) return false;
|
|
134
|
+
const raw = await fs.readFile(credsPath, "utf-8");
|
|
135
|
+
JSON.parse(raw);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.warn(`${TAG} credential validation failed path=${credsPath}: ${String(err)}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function readCredsJsonRaw(filePath) {
|
|
143
|
+
try {
|
|
144
|
+
if (!fsSync.existsSync(filePath)) return null;
|
|
145
|
+
const stats = fsSync.statSync(filePath);
|
|
146
|
+
if (!stats.isFile() || stats.size <= 1) return null;
|
|
147
|
+
return fsSync.readFileSync(filePath, "utf-8");
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function maybeRestoreCredsFromBackup(authDir) {
|
|
153
|
+
try {
|
|
154
|
+
const credsPath = resolveCredsPath(authDir);
|
|
155
|
+
const backupPath = resolveCredsBackupPath(authDir);
|
|
156
|
+
const raw = readCredsJsonRaw(credsPath);
|
|
157
|
+
if (raw) {
|
|
158
|
+
JSON.parse(raw);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const backupRaw = readCredsJsonRaw(backupPath);
|
|
162
|
+
if (!backupRaw) return;
|
|
163
|
+
JSON.parse(backupRaw);
|
|
164
|
+
fsSync.copyFileSync(backupPath, credsPath);
|
|
165
|
+
console.error(`${TAG} restored corrupted creds.json from backup in ${authDir}`);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn(`${TAG} credential restore failed authDir=${authDir}: ${String(err)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function readSelfId(authDir) {
|
|
171
|
+
try {
|
|
172
|
+
const credsPath = resolveCredsPath(authDir);
|
|
173
|
+
if (!fsSync.existsSync(credsPath)) return { e164: null, jid: null, lid: null };
|
|
174
|
+
const raw = fsSync.readFileSync(credsPath, "utf-8");
|
|
175
|
+
const parsed = JSON.parse(raw);
|
|
176
|
+
const jid = parsed?.me?.id ?? null;
|
|
177
|
+
const lidRaw = parsed?.me?.lid ?? null;
|
|
178
|
+
let e164 = null;
|
|
179
|
+
if (jid) {
|
|
180
|
+
const match = jid.match(/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i);
|
|
181
|
+
if (match) e164 = match[1];
|
|
182
|
+
}
|
|
183
|
+
let lid = null;
|
|
184
|
+
if (lidRaw) {
|
|
185
|
+
const lidMatch = lidRaw.match(/^(\d+)(?::\d+)?@lid$/i);
|
|
186
|
+
if (lidMatch) lid = `${lidMatch[1]}@lid`;
|
|
187
|
+
}
|
|
188
|
+
return { e164, jid, lid };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.warn(`${TAG} readSelfId failed authDir=${authDir}: ${String(err)}`);
|
|
191
|
+
return { e164: null, jid: null, lid: null };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function credsCensus(authDir) {
|
|
195
|
+
const raw = readCredsJsonRaw(resolveCredsPath(authDir));
|
|
196
|
+
if (!raw) return { credsPresent: false, registered: false, hasAccountSignature: false, e164: null };
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(raw);
|
|
199
|
+
const registered = parsed?.registered === true;
|
|
200
|
+
const hasAccountSignature = parsed?.account != null;
|
|
201
|
+
let e164 = null;
|
|
202
|
+
const jid = parsed?.me?.id;
|
|
203
|
+
if (jid) {
|
|
204
|
+
const match = jid.match(/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i);
|
|
205
|
+
if (match) e164 = match[1];
|
|
206
|
+
}
|
|
207
|
+
return { credsPresent: true, registered, hasAccountSignature, e164 };
|
|
208
|
+
} catch {
|
|
209
|
+
return { credsPresent: true, registered: false, hasAccountSignature: false, e164: null };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function getAuthAgeMs(authDir) {
|
|
213
|
+
try {
|
|
214
|
+
const stats = fsSync.statSync(resolveCredsPath(authDir));
|
|
215
|
+
return Date.now() - stats.mtimeMs;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function clearAuth(authDir) {
|
|
221
|
+
try {
|
|
222
|
+
await fs.access(authDir);
|
|
223
|
+
} catch {
|
|
224
|
+
console.error(`${TAG} no auth dir found at ${authDir}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
await fs.rm(authDir, { recursive: true, force: true });
|
|
228
|
+
console.error(`${TAG} cleared credentials at ${authDir}`);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
function discardAuthDirSync(authDir) {
|
|
232
|
+
try {
|
|
233
|
+
if (!fsSync.existsSync(authDir)) return false;
|
|
234
|
+
fsSync.rmSync(authDir, { recursive: true, force: true });
|
|
235
|
+
console.error(`${TAG} discarded credential dir ${authDir}`);
|
|
236
|
+
return true;
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.warn(`${TAG} discardAuthDirSync failed authDir=${authDir}: ${String(err)}`);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function listCredentialAccountIds(configDir2) {
|
|
243
|
+
const credsRoot = path.join(os.homedir(), configDir2, "credentials", "whatsapp");
|
|
244
|
+
try {
|
|
245
|
+
const entries = fsSync.readdirSync(credsRoot, { withFileTypes: true });
|
|
246
|
+
return entries.filter((e) => e.isDirectory() && hasCredsSync(path.join(credsRoot, e.name))).map((e) => e.name);
|
|
247
|
+
} catch {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// app/lib/whatsapp/session.ts
|
|
253
|
+
import { randomUUID } from "crypto";
|
|
254
|
+
import fsSync2 from "fs";
|
|
255
|
+
import fs3 from "fs/promises";
|
|
256
|
+
import { inspect } from "util";
|
|
257
|
+
import {
|
|
258
|
+
Browsers,
|
|
259
|
+
DisconnectReason,
|
|
260
|
+
fetchLatestBaileysVersion,
|
|
261
|
+
makeCacheableSignalKeyStore,
|
|
262
|
+
makeWASocket,
|
|
263
|
+
useMultiFileAuthState
|
|
264
|
+
} from "@whiskeysockets/baileys";
|
|
265
|
+
import { createRequire } from "module";
|
|
266
|
+
|
|
267
|
+
// app/lib/whatsapp/baileys-debug-window.ts
|
|
268
|
+
import fs2 from "fs";
|
|
269
|
+
import path2 from "path";
|
|
270
|
+
var TAG2 = "[whatsapp:session]";
|
|
271
|
+
var SENTINEL_BASENAME = "baileys-debug-until";
|
|
272
|
+
var STAT_INTERVAL_MS = 1e3;
|
|
273
|
+
var VALID_LEVELS = /* @__PURE__ */ new Set(["trace", "debug", "info"]);
|
|
274
|
+
var INACTIVE = { active: false, level: "warn" };
|
|
275
|
+
var cache = /* @__PURE__ */ new Map();
|
|
276
|
+
function sentinelPath(authDir) {
|
|
277
|
+
return path2.join(authDir, SENTINEL_BASENAME);
|
|
278
|
+
}
|
|
279
|
+
function readSentinel(authDir, nowMs) {
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = fs2.readFileSync(sentinelPath(authDir), "utf-8");
|
|
283
|
+
} catch {
|
|
284
|
+
return { state: INACTIVE, deadlineMs: null };
|
|
285
|
+
}
|
|
286
|
+
const line = raw.split("\n", 1)[0].trim();
|
|
287
|
+
if (!line) return { state: INACTIVE, deadlineMs: null };
|
|
288
|
+
const tokens = line.split(/\s+/);
|
|
289
|
+
const deadlineMs = Date.parse(tokens[0]);
|
|
290
|
+
if (Number.isNaN(deadlineMs) || deadlineMs <= nowMs) {
|
|
291
|
+
return { state: INACTIVE, deadlineMs: null };
|
|
292
|
+
}
|
|
293
|
+
let level = "trace";
|
|
294
|
+
if (tokens.length > 1) {
|
|
295
|
+
if (!VALID_LEVELS.has(tokens[1])) return { state: INACTIVE, deadlineMs: null };
|
|
296
|
+
level = tokens[1];
|
|
297
|
+
}
|
|
298
|
+
return { state: { active: true, level }, deadlineMs };
|
|
299
|
+
}
|
|
300
|
+
function resolveDebugWindow(authDir, nowMs = Date.now()) {
|
|
301
|
+
if (!authDir) return INACTIVE;
|
|
302
|
+
const prev = cache.get(authDir);
|
|
303
|
+
if (prev && nowMs - prev.checkedAtMs < STAT_INTERVAL_MS) {
|
|
304
|
+
return prev.state;
|
|
305
|
+
}
|
|
306
|
+
const { state, deadlineMs } = readSentinel(authDir, nowMs);
|
|
307
|
+
const wasActive = prev?.state.active ?? false;
|
|
308
|
+
if (!wasActive && state.active) {
|
|
309
|
+
console.error(
|
|
310
|
+
`${TAG2} op=baileys-debug-window-open account=${path2.basename(authDir)} level=${state.level} until=${new Date(deadlineMs).toISOString()}`
|
|
311
|
+
);
|
|
312
|
+
} else if (wasActive && !state.active) {
|
|
313
|
+
console.error(
|
|
314
|
+
`${TAG2} op=baileys-debug-window-close account=${path2.basename(authDir)}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
cache.set(authDir, { checkedAtMs: nowMs, state });
|
|
318
|
+
return state;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// app/lib/whatsapp/ingest-ledger.ts
|
|
322
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, readdirSync } from "fs";
|
|
323
|
+
import { join } from "path";
|
|
324
|
+
var TAG3 = "[wa-ingest-ledger]";
|
|
325
|
+
var LEDGER_ROOT = () => join(process.env.MAXY_DIR_OVERRIDE ?? MAXY_DIR, "data", "whatsapp-ingest-ledger");
|
|
326
|
+
var ensuredRoots = /* @__PURE__ */ new Set();
|
|
327
|
+
function ledgerFilePath(account) {
|
|
328
|
+
const root = LEDGER_ROOT();
|
|
329
|
+
if (!ensuredRoots.has(root)) {
|
|
330
|
+
if (!existsSync(root)) mkdirSync(root, { recursive: true });
|
|
331
|
+
ensuredRoots.add(root);
|
|
332
|
+
}
|
|
333
|
+
return join(root, `${account}.jsonl`);
|
|
334
|
+
}
|
|
335
|
+
function appendStanza(entry) {
|
|
336
|
+
try {
|
|
337
|
+
const row = {
|
|
338
|
+
msgId: entry.msgId,
|
|
339
|
+
from: entry.from,
|
|
340
|
+
type: entry.type,
|
|
341
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
342
|
+
};
|
|
343
|
+
appendFileSync(ledgerFilePath(entry.account), JSON.stringify(row) + "\n");
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error(
|
|
346
|
+
`${TAG3} FAIL op=append msgId=${entry.msgId} err=${err instanceof Error ? err.message : String(err)}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function markDeclined(entry) {
|
|
351
|
+
try {
|
|
352
|
+
const row = {
|
|
353
|
+
declined: entry.msgId,
|
|
354
|
+
reason: entry.reason,
|
|
355
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
356
|
+
};
|
|
357
|
+
appendFileSync(ledgerFilePath(entry.account), JSON.stringify(row) + "\n");
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(
|
|
360
|
+
`${TAG3} FAIL op=decline msgId=${entry.msgId} err=${err instanceof Error ? err.message : String(err)}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function readDeclined(account) {
|
|
365
|
+
const path3 = ledgerFilePath(account);
|
|
366
|
+
const out = /* @__PURE__ */ new Set();
|
|
367
|
+
if (!existsSync(path3)) return out;
|
|
368
|
+
for (const line of readFileSync(path3, "utf8").split("\n")) {
|
|
369
|
+
if (!line.trim()) continue;
|
|
370
|
+
try {
|
|
371
|
+
const row = JSON.parse(line);
|
|
372
|
+
if (typeof row.declined === "string") out.add(row.declined);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
function appendNotifyEligible(entry) {
|
|
379
|
+
try {
|
|
380
|
+
const row = {
|
|
381
|
+
notifyEligible: entry.msgId,
|
|
382
|
+
upsertType: entry.upsertType,
|
|
383
|
+
fromMe: entry.fromMe,
|
|
384
|
+
allowed: entry.allowed,
|
|
385
|
+
reason: entry.reason,
|
|
386
|
+
passive: entry.passive,
|
|
387
|
+
persistAccount: entry.persistAccount,
|
|
388
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
389
|
+
};
|
|
390
|
+
appendFileSync(ledgerFilePath(entry.account), JSON.stringify(row) + "\n");
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(
|
|
393
|
+
`${TAG3} FAIL op=append-notify-eligible msgId=${entry.msgId} err=${err instanceof Error ? err.message : String(err)}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function appendNotifyPublished(entry) {
|
|
398
|
+
try {
|
|
399
|
+
const row = {
|
|
400
|
+
notifyPublished: entry.msgId,
|
|
401
|
+
subscribers: entry.subscribers,
|
|
402
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
403
|
+
};
|
|
404
|
+
appendFileSync(ledgerFilePath(entry.account), JSON.stringify(row) + "\n");
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error(
|
|
407
|
+
`${TAG3} FAIL op=append-notify-published msgId=${entry.msgId} err=${err instanceof Error ? err.message : String(err)}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function readNotifyRows(account) {
|
|
412
|
+
const path3 = ledgerFilePath(account);
|
|
413
|
+
const out = {
|
|
414
|
+
eligible: [],
|
|
415
|
+
published: []
|
|
416
|
+
};
|
|
417
|
+
if (!existsSync(path3)) return out;
|
|
418
|
+
for (const line of readFileSync(path3, "utf8").split("\n")) {
|
|
419
|
+
if (!line.trim()) continue;
|
|
420
|
+
try {
|
|
421
|
+
const row = JSON.parse(line);
|
|
422
|
+
if (typeof row.notifyEligible === "string") {
|
|
423
|
+
out.eligible.push(row);
|
|
424
|
+
} else if (typeof row.notifyPublished === "string") {
|
|
425
|
+
out.published.push(row);
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
function readLedger(account) {
|
|
433
|
+
const path3 = ledgerFilePath(account);
|
|
434
|
+
if (!existsSync(path3)) return [];
|
|
435
|
+
const out = [];
|
|
436
|
+
for (const line of readFileSync(path3, "utf8").split("\n")) {
|
|
437
|
+
if (!line.trim()) continue;
|
|
438
|
+
try {
|
|
439
|
+
const row = JSON.parse(line);
|
|
440
|
+
if (row && typeof row.msgId === "string") out.push(row);
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
function pruneLedger(account, olderThanMs) {
|
|
447
|
+
const path3 = ledgerFilePath(account);
|
|
448
|
+
if (!existsSync(path3)) return 0;
|
|
449
|
+
const lines = readFileSync(path3, "utf8").split("\n").filter((l) => l.trim());
|
|
450
|
+
const cutoff = Date.now() - olderThanMs;
|
|
451
|
+
const kept = lines.filter((line) => {
|
|
452
|
+
try {
|
|
453
|
+
const row = JSON.parse(line);
|
|
454
|
+
const t = typeof row.ts === "string" ? Date.parse(row.ts) : NaN;
|
|
455
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
if (kept.length === lines.length) return 0;
|
|
461
|
+
writeFileSync(path3, kept.join("\n") + (kept.length ? "\n" : ""));
|
|
462
|
+
return lines.length - kept.length;
|
|
463
|
+
}
|
|
464
|
+
function listLedgerAccounts() {
|
|
465
|
+
const root = LEDGER_ROOT();
|
|
466
|
+
if (!existsSync(root)) return [];
|
|
467
|
+
try {
|
|
468
|
+
return readdirSync(root).filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
|
|
469
|
+
} catch {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// app/lib/whatsapp/session.ts
|
|
475
|
+
var TAG4 = "[whatsapp:session]";
|
|
476
|
+
var WIRE_TAG = "[whatsapp:wire]";
|
|
477
|
+
var baileysPkgVersion = (() => {
|
|
478
|
+
try {
|
|
479
|
+
const req = createRequire(import.meta.url);
|
|
480
|
+
return req("@whiskeysockets/baileys/package.json").version ?? "unknown";
|
|
481
|
+
} catch {
|
|
482
|
+
return "unknown";
|
|
483
|
+
}
|
|
484
|
+
})();
|
|
485
|
+
console.error(`${TAG4} baileys package loaded version=${baileysPkgVersion}`);
|
|
486
|
+
var __BAILEYS_TAG = "[whatsapp:baileys]";
|
|
487
|
+
var INSPECT_OPTS = {
|
|
488
|
+
depth: null,
|
|
489
|
+
breakLength: Infinity,
|
|
490
|
+
compact: true,
|
|
491
|
+
maxArrayLength: 100,
|
|
492
|
+
maxStringLength: 1e4
|
|
493
|
+
};
|
|
494
|
+
function serializeArg(arg) {
|
|
495
|
+
if (typeof arg === "string") return arg;
|
|
496
|
+
try {
|
|
497
|
+
return inspect(arg, INSPECT_OPTS);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return `[inspect-failed: ${String(err)}]`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function bindingsToPrefix(bindings) {
|
|
503
|
+
const parts = [];
|
|
504
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
505
|
+
const rendered = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : serializeArg(value);
|
|
506
|
+
parts.push(`${key}=${rendered}`);
|
|
507
|
+
}
|
|
508
|
+
return parts.join(" ");
|
|
509
|
+
}
|
|
510
|
+
var LEVEL_RANK = { trace: 10, debug: 20, info: 30 };
|
|
511
|
+
function createBaileysLogger(bindings = {}, authDir) {
|
|
512
|
+
const prefix = Object.keys(bindings).length > 0 ? bindingsToPrefix(bindings) : "";
|
|
513
|
+
const emit = (level, args) => {
|
|
514
|
+
const serialized = args.map(serializeArg).join(" ");
|
|
515
|
+
if (prefix) {
|
|
516
|
+
console.error(__BAILEYS_TAG, level, prefix, serialized);
|
|
517
|
+
} else {
|
|
518
|
+
console.error(__BAILEYS_TAG, level, serialized);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const emitIfWindowed = (rank, level, args) => {
|
|
522
|
+
const w = resolveDebugWindow(authDir);
|
|
523
|
+
if (w.active && rank >= LEVEL_RANK[w.level]) {
|
|
524
|
+
emit(level, args);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
// Live getter: Baileys reads config.logger.level per-frame and (verified on
|
|
529
|
+
// the pinned version) never shallow-copies the logger, so the window's level
|
|
530
|
+
// reaches socket.js:449/465 as it changes. A Baileys that spread the logger
|
|
531
|
+
// would freeze this to one value — revisit on upgrade.
|
|
532
|
+
get level() {
|
|
533
|
+
return resolveDebugWindow(authDir).level;
|
|
534
|
+
},
|
|
535
|
+
trace: (...args) => emitIfWindowed(LEVEL_RANK.trace, "TRACE", args),
|
|
536
|
+
debug: (...args) => emitIfWindowed(LEVEL_RANK.debug, "DEBUG", args),
|
|
537
|
+
info: (...args) => emitIfWindowed(LEVEL_RANK.info, "INFO", args),
|
|
538
|
+
warn: (...args) => emit("WARN", args),
|
|
539
|
+
error: (...args) => emit("ERROR", args),
|
|
540
|
+
fatal: (...args) => emit("FATAL", args),
|
|
541
|
+
child: (childBindings) => createBaileysLogger({ ...bindings, ...childBindings ?? {} }, authDir)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
var credsSaveQueue = Promise.resolve();
|
|
545
|
+
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
546
|
+
console.error(`${TAG4} draining credential save queue\u2026`);
|
|
547
|
+
const timer = new Promise(
|
|
548
|
+
(resolve6) => setTimeout(() => resolve6("timeout"), timeoutMs)
|
|
549
|
+
);
|
|
550
|
+
const result = await Promise.race([
|
|
551
|
+
credsSaveQueue.then(() => "drained"),
|
|
552
|
+
timer
|
|
553
|
+
]);
|
|
554
|
+
if (result === "timeout") {
|
|
555
|
+
console.error(`${TAG4} credential save queue drain timed out after ${timeoutMs}ms`);
|
|
556
|
+
} else {
|
|
557
|
+
console.error(`${TAG4} credential save queue drained`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function readCredsJsonRaw2(filePath) {
|
|
561
|
+
try {
|
|
562
|
+
if (!fsSync2.existsSync(filePath)) return null;
|
|
563
|
+
const stats = fsSync2.statSync(filePath);
|
|
564
|
+
if (!stats.isFile() || stats.size <= 1) return null;
|
|
565
|
+
return fsSync2.readFileSync(filePath, "utf-8");
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function safeSaveCreds(authDir, saveCreds) {
|
|
571
|
+
try {
|
|
572
|
+
const credsPath = resolveCredsPath(authDir);
|
|
573
|
+
const backupPath = resolveCredsBackupPath(authDir);
|
|
574
|
+
const raw = readCredsJsonRaw2(credsPath);
|
|
575
|
+
if (raw) {
|
|
576
|
+
try {
|
|
577
|
+
JSON.parse(raw);
|
|
578
|
+
fsSync2.copyFileSync(credsPath, backupPath);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
console.warn(`${TAG4} backup pre-copy failed (parse or write) authDir=${authDir}: ${String(err)}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.warn(`${TAG4} backup preparation failed authDir=${authDir}: ${String(err)}`);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
await Promise.resolve(saveCreds());
|
|
588
|
+
const credsPath = resolveCredsPath(authDir);
|
|
589
|
+
try {
|
|
590
|
+
const stats = await fs3.stat(credsPath);
|
|
591
|
+
if (stats.size <= 1) {
|
|
592
|
+
console.error(`${TAG4} WARNING: creds file empty/truncated after save path=${credsPath} size=${stats.size}`);
|
|
593
|
+
} else {
|
|
594
|
+
console.error(`${TAG4} creds saved and verified path=${credsPath} size=${stats.size}`);
|
|
595
|
+
}
|
|
596
|
+
} catch (statErr) {
|
|
597
|
+
console.error(`${TAG4} creds NOT found on disk after save path=${credsPath}: ${String(statErr)}`);
|
|
598
|
+
}
|
|
599
|
+
} catch (err) {
|
|
600
|
+
console.error(`${TAG4} failed saving creds to ${authDir}: ${String(err)}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function enqueueSaveCreds(authDir, saveCreds) {
|
|
604
|
+
credsSaveQueue = credsSaveQueue.then(() => safeSaveCreds(authDir, saveCreds)).catch((err) => {
|
|
605
|
+
console.error(`${TAG4} creds save queue error: ${String(err)}`);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async function createWaSocket(opts) {
|
|
609
|
+
const { authDir, onQr, onConnectionUpdate, silent, correlationId, account, getCodeIssuedTs } = opts;
|
|
610
|
+
const cid = correlationId ?? "none";
|
|
611
|
+
const acct = account ?? "unknown";
|
|
612
|
+
await fs3.mkdir(authDir, { recursive: true });
|
|
613
|
+
maybeRestoreCredsFromBackup(authDir);
|
|
614
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
615
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
616
|
+
if (!silent) {
|
|
617
|
+
const preCensus = credsCensus(authDir);
|
|
618
|
+
console.error(
|
|
619
|
+
`${TAG4} op=creds-pre cid=${cid} account=${acct} credsPresent=${preCensus.credsPresent} registered=${preCensus.registered} hasAccount=${preCensus.hasAccountSignature}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const openTs = Date.now();
|
|
623
|
+
console.error(
|
|
624
|
+
`${TAG4} op=socket-open cid=${cid} account=${acct} baileysVersion=${baileysPkgVersion} waProtocolVersion=${version.join(".")} openTs=${openTs}`
|
|
625
|
+
);
|
|
626
|
+
const baileysLogger = createBaileysLogger({}, authDir);
|
|
627
|
+
const sock = makeWASocket({
|
|
628
|
+
auth: {
|
|
629
|
+
creds: state.creds,
|
|
630
|
+
keys: makeCacheableSignalKeyStore(state.keys, baileysLogger)
|
|
631
|
+
},
|
|
632
|
+
version,
|
|
633
|
+
logger: baileysLogger,
|
|
634
|
+
printQRInTerminal: false,
|
|
635
|
+
browser: Browsers.macOS("Chrome"),
|
|
636
|
+
syncFullHistory: false,
|
|
637
|
+
markOnlineOnConnect: false,
|
|
638
|
+
fireInitQueries: false
|
|
639
|
+
});
|
|
640
|
+
sock.ev.on("creds.update", () => {
|
|
641
|
+
console.error(`${TAG4} creds.update received \u2014 saving to ${authDir}`);
|
|
642
|
+
enqueueSaveCreds(authDir, saveCreds);
|
|
643
|
+
});
|
|
644
|
+
let qrSequence = 0;
|
|
645
|
+
let everOpen = false;
|
|
646
|
+
let lastQrTs = 0;
|
|
647
|
+
let pairSuccessLogged = false;
|
|
648
|
+
sock.ev.on("connection.update", (update) => {
|
|
649
|
+
try {
|
|
650
|
+
const parts = [];
|
|
651
|
+
if (update.connection) parts.push(`connection=${update.connection}`);
|
|
652
|
+
if (update.qr) parts.push(`qr=#${++qrSequence}`);
|
|
653
|
+
if (update.isNewLogin !== void 0) parts.push(`isNewLogin=${update.isNewLogin}`);
|
|
654
|
+
if (update.receivedPendingNotifications !== void 0) parts.push(`pendingNotifications=${update.receivedPendingNotifications}`);
|
|
655
|
+
if (update.connection === "close" && update.lastDisconnect) {
|
|
656
|
+
const status = getStatusCode(update.lastDisconnect.error);
|
|
657
|
+
parts.push(`disconnectStatus=${status ?? "unknown"}`);
|
|
658
|
+
parts.push(`disconnectReason=${formatError(update.lastDisconnect.error)}`);
|
|
659
|
+
}
|
|
660
|
+
if (!silent && parts.length > 0) {
|
|
661
|
+
console.error(`${TAG4} connection.update ${parts.join(" ")}`);
|
|
662
|
+
}
|
|
663
|
+
if (update.qr && !silent) {
|
|
664
|
+
const now = Date.now();
|
|
665
|
+
const sinceLastRefMs = lastQrTs ? now - lastQrTs : null;
|
|
666
|
+
lastQrTs = now;
|
|
667
|
+
console.error(
|
|
668
|
+
`${TAG4} op=qr cid=${cid} qr=#${qrSequence} sinceOpenMs=${now - openTs} sinceLastRefMs=${sinceLastRefMs ?? "n/a"}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (update.isNewLogin === true && !pairSuccessLogged && !silent) {
|
|
672
|
+
pairSuccessLogged = true;
|
|
673
|
+
const codeIssuedTs = getCodeIssuedTs?.();
|
|
674
|
+
const sinceCodeIssuedMs = codeIssuedTs ? Date.now() - codeIssuedTs : null;
|
|
675
|
+
const afterCensus = credsCensus(authDir);
|
|
676
|
+
console.error(
|
|
677
|
+
`${TAG4} op=pair-success cid=${cid} account=${acct} sinceCodeIssuedMs=${sinceCodeIssuedMs ?? "n/a"} hasAccountAfter=${afterCensus.hasAccountSignature}`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (update.qr && onQr) {
|
|
681
|
+
onQr(update.qr);
|
|
682
|
+
}
|
|
683
|
+
if (update.connection === "close") {
|
|
684
|
+
const status = getStatusCode(update.lastDisconnect?.error);
|
|
685
|
+
if (!silent) {
|
|
686
|
+
console.error(
|
|
687
|
+
`${TAG4} op=close cid=${cid} account=${acct} status=${status ?? "unknown"} reason="${formatError(update.lastDisconnect?.error)}" socketLifeMs=${Date.now() - openTs} kind=${closeKind(status, everOpen)}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (status === DisconnectReason.loggedOut && !silent) {
|
|
691
|
+
console.error(`${TAG4} session logged out (401) \u2014 re-link required`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (update.connection === "open") {
|
|
695
|
+
everOpen = true;
|
|
696
|
+
if (!silent) console.error(`${TAG4} connected`);
|
|
697
|
+
}
|
|
698
|
+
onConnectionUpdate?.(update);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(`${TAG4} connection.update handler error: ${String(err)}`);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
if (sock.ws && typeof sock.ws.on === "function") {
|
|
704
|
+
sock.ws.on("error", (err) => {
|
|
705
|
+
console.error(`${TAG4} WebSocket error: ${String(err)}`);
|
|
706
|
+
});
|
|
707
|
+
sock.ws.on("CB:message", (node) => {
|
|
708
|
+
const attrs = node?.attrs ?? {};
|
|
709
|
+
console.error(
|
|
710
|
+
`${WIRE_TAG} op=stanza msgId=${attrs.id ?? "none"} from=${attrs.from ?? "none"} type=${attrs.type ?? "none"} offline=${attrs.offline ?? "none"} account=${acct}`
|
|
711
|
+
);
|
|
712
|
+
if (attrs.id) {
|
|
713
|
+
appendStanza({
|
|
714
|
+
msgId: String(attrs.id),
|
|
715
|
+
from: String(attrs.from ?? "none"),
|
|
716
|
+
type: String(attrs.type ?? "none"),
|
|
717
|
+
account: acct
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return sock;
|
|
723
|
+
}
|
|
724
|
+
async function waitForConnection(sock) {
|
|
725
|
+
return new Promise((resolve6, reject) => {
|
|
726
|
+
const handler = (update) => {
|
|
727
|
+
if (update.connection === "open") {
|
|
728
|
+
sock.ev.off("connection.update", handler);
|
|
729
|
+
resolve6();
|
|
730
|
+
}
|
|
731
|
+
if (update.connection === "close") {
|
|
732
|
+
sock.ev.off("connection.update", handler);
|
|
733
|
+
reject(update.lastDisconnect ?? new Error("Connection closed"));
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
sock.ev.on("connection.update", handler);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function getStatusCode(err) {
|
|
740
|
+
return err?.error?.output?.statusCode ?? err?.output?.statusCode ?? err?.status;
|
|
741
|
+
}
|
|
742
|
+
function formatError(err) {
|
|
743
|
+
if (err instanceof Error) return err.message;
|
|
744
|
+
if (typeof err === "string") return err;
|
|
745
|
+
if (!err || typeof err !== "object") return String(err);
|
|
746
|
+
const boom = extractBoomDetails(err) ?? extractBoomDetails(err?.error) ?? extractBoomDetails(err?.lastDisconnect?.error);
|
|
747
|
+
const status = boom?.statusCode ?? getStatusCode(err);
|
|
748
|
+
const code = err?.code;
|
|
749
|
+
const codeText = typeof code === "string" || typeof code === "number" ? String(code) : void 0;
|
|
750
|
+
const messageCandidates = [
|
|
751
|
+
boom?.message,
|
|
752
|
+
typeof err?.message === "string" ? err.message : void 0,
|
|
753
|
+
typeof err?.error?.message === "string" ? err.error.message : void 0
|
|
754
|
+
].filter((v) => Boolean(v?.trim()));
|
|
755
|
+
const message = messageCandidates[0];
|
|
756
|
+
const pieces = [];
|
|
757
|
+
if (typeof status === "number") pieces.push(`status=${status}`);
|
|
758
|
+
if (boom?.error) pieces.push(boom.error);
|
|
759
|
+
if (message) pieces.push(message);
|
|
760
|
+
if (codeText) pieces.push(`code=${codeText}`);
|
|
761
|
+
return pieces.length > 0 ? pieces.join(" ") : String(err);
|
|
762
|
+
}
|
|
763
|
+
function extractBoomDetails(err) {
|
|
764
|
+
if (!err || typeof err !== "object") return null;
|
|
765
|
+
const output = err?.output;
|
|
766
|
+
if (!output || typeof output !== "object") return null;
|
|
767
|
+
const payload = output.payload;
|
|
768
|
+
const statusCode = typeof output.statusCode === "number" ? output.statusCode : typeof payload?.statusCode === "number" ? payload.statusCode : void 0;
|
|
769
|
+
const error = typeof payload?.error === "string" ? payload.error : void 0;
|
|
770
|
+
const message = typeof payload?.message === "string" ? payload.message : void 0;
|
|
771
|
+
if (!statusCode && !error && !message) return null;
|
|
772
|
+
return { statusCode, error, message };
|
|
773
|
+
}
|
|
774
|
+
function closeKind(status, everOpen) {
|
|
775
|
+
if (status === DisconnectReason.restartRequired) return "restart-required-515";
|
|
776
|
+
if (status === DisconnectReason.loggedOut) return "logged-out-401";
|
|
777
|
+
if (status === 408) return everOpen ? "conn-lost-408" : "qr-refs-ended-408";
|
|
778
|
+
return "other";
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// app/lib/whatsapp/reconnect.ts
|
|
782
|
+
import { DisconnectReason as DisconnectReason2 } from "@whiskeysockets/baileys";
|
|
783
|
+
var STABLE_UPTIME_MS = 3e4;
|
|
784
|
+
function computeReconnectNextState(input) {
|
|
785
|
+
const { reconnectAttempts, uptimeMs, stableThresholdMs, maxAttempts } = input;
|
|
786
|
+
if (uptimeMs >= stableThresholdMs) {
|
|
787
|
+
return { action: "retry", nextAttempts: 0, reason: "stable-reset" };
|
|
788
|
+
}
|
|
789
|
+
const nextAttempts = reconnectAttempts + 1;
|
|
790
|
+
if (nextAttempts >= maxAttempts) {
|
|
791
|
+
return { action: "abort", finalAttempts: maxAttempts };
|
|
792
|
+
}
|
|
793
|
+
return { action: "retry", nextAttempts, reason: "short-lived" };
|
|
794
|
+
}
|
|
795
|
+
function classifyDisconnect(err) {
|
|
796
|
+
const statusCode = getStatusCode(err);
|
|
797
|
+
const message = formatError(err);
|
|
798
|
+
if (statusCode === DisconnectReason2.loggedOut) {
|
|
799
|
+
return {
|
|
800
|
+
kind: "loggedOut",
|
|
801
|
+
statusCode,
|
|
802
|
+
message: "Session logged out \u2014 re-link required",
|
|
803
|
+
shouldRetry: false
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (statusCode === 409 || statusCode === DisconnectReason2.connectionReplaced) {
|
|
807
|
+
return {
|
|
808
|
+
kind: "conflict",
|
|
809
|
+
statusCode,
|
|
810
|
+
message: "Connection replaced by another client",
|
|
811
|
+
shouldRetry: false
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
kind: "transient",
|
|
816
|
+
statusCode,
|
|
817
|
+
message,
|
|
818
|
+
shouldRetry: true
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function computeBackoff(attempt, config = {}) {
|
|
822
|
+
const base = config.baseDelayMs ?? 2e3;
|
|
823
|
+
const max = config.maxDelayMs ?? 12e4;
|
|
824
|
+
const exponential = Math.min(base * Math.pow(2, attempt), max);
|
|
825
|
+
const jitter = exponential * (0.75 + Math.random() * 0.5);
|
|
826
|
+
return Math.round(jitter);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// app/lib/whatsapp/init-queries.ts
|
|
830
|
+
import { inspect as inspect2 } from "util";
|
|
831
|
+
|
|
832
|
+
// app/lib/whatsapp/with-timeout.ts
|
|
833
|
+
function isTimeoutError(err) {
|
|
834
|
+
return Boolean(err?.__maxyTimeout);
|
|
835
|
+
}
|
|
836
|
+
function withTimeout(label, promise, timeoutMs) {
|
|
837
|
+
return new Promise((resolve6, reject) => {
|
|
838
|
+
const timer = setTimeout(() => {
|
|
839
|
+
const err = new Error(`${label} timed out after ${timeoutMs}ms`);
|
|
840
|
+
err.__maxyTimeout = true;
|
|
841
|
+
reject(err);
|
|
842
|
+
}, timeoutMs);
|
|
843
|
+
timer.unref?.();
|
|
844
|
+
promise.then(
|
|
845
|
+
(value) => {
|
|
846
|
+
clearTimeout(timer);
|
|
847
|
+
resolve6(value);
|
|
848
|
+
},
|
|
849
|
+
(err) => {
|
|
850
|
+
clearTimeout(timer);
|
|
851
|
+
reject(err);
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// app/lib/whatsapp/init-queries.ts
|
|
858
|
+
var TAG5 = "[whatsapp:init]";
|
|
859
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 15e3;
|
|
860
|
+
var INSPECT_OPTS2 = {
|
|
861
|
+
depth: null,
|
|
862
|
+
breakLength: Infinity,
|
|
863
|
+
compact: true,
|
|
864
|
+
maxArrayLength: 50
|
|
865
|
+
};
|
|
866
|
+
function formatErr(err) {
|
|
867
|
+
if (err instanceof Error) {
|
|
868
|
+
const base = `${err.name}: ${err.message}`;
|
|
869
|
+
const inspected = inspect2(err, INSPECT_OPTS2);
|
|
870
|
+
return `${base}
|
|
871
|
+
${inspected}`;
|
|
872
|
+
}
|
|
873
|
+
return inspect2(err, INSPECT_OPTS2);
|
|
874
|
+
}
|
|
875
|
+
async function runInitQueries(sock, ctx) {
|
|
876
|
+
const startedAt = Date.now();
|
|
877
|
+
const queryTimeoutMs = ctx.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
|
|
878
|
+
const result = {
|
|
879
|
+
blocklistOk: false,
|
|
880
|
+
privacySettingsOk: false,
|
|
881
|
+
aborted: false,
|
|
882
|
+
errors: {},
|
|
883
|
+
elapsedMs: 0
|
|
884
|
+
};
|
|
885
|
+
console.error(
|
|
886
|
+
`${TAG5} runInitQueries start account=${ctx.accountId} attempt=${ctx.attempt} selfPhone=${ctx.selfPhone ?? "unknown"} authDir=${ctx.authDir} queryTimeoutMs=${queryTimeoutMs}`
|
|
887
|
+
);
|
|
888
|
+
console.error(
|
|
889
|
+
`${TAG5} fetchProps SKIPPED account=${ctx.accountId} reason=no-platform-consumer`
|
|
890
|
+
);
|
|
891
|
+
if (ctx.signal?.aborted) {
|
|
892
|
+
result.aborted = true;
|
|
893
|
+
result.elapsedMs = Date.now() - startedAt;
|
|
894
|
+
console.error(
|
|
895
|
+
`${TAG5} runInitQueries aborted before start account=${ctx.accountId}`
|
|
896
|
+
);
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
console.error(`${TAG5} fetchBlocklist start account=${ctx.accountId}`);
|
|
900
|
+
try {
|
|
901
|
+
const blocklist = await withTimeout(
|
|
902
|
+
"fetchBlocklist",
|
|
903
|
+
sock.fetchBlocklist(),
|
|
904
|
+
queryTimeoutMs
|
|
905
|
+
);
|
|
906
|
+
const count = Array.isArray(blocklist) ? blocklist.length : 0;
|
|
907
|
+
result.blocklistOk = true;
|
|
908
|
+
console.error(
|
|
909
|
+
`${TAG5} fetchBlocklist ok account=${ctx.accountId} count=${count}`
|
|
910
|
+
);
|
|
911
|
+
} catch (err) {
|
|
912
|
+
const formatted = formatErr(err);
|
|
913
|
+
result.errors.blocklist = formatted;
|
|
914
|
+
console.error(
|
|
915
|
+
`${TAG5} fetchBlocklist FAILED account=${ctx.accountId}: ${formatted}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (ctx.signal?.aborted) {
|
|
919
|
+
result.aborted = true;
|
|
920
|
+
result.elapsedMs = Date.now() - startedAt;
|
|
921
|
+
console.error(
|
|
922
|
+
`${TAG5} runInitQueries aborted mid-sequence account=${ctx.accountId} after=fetchBlocklist`
|
|
923
|
+
);
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
console.error(`${TAG5} fetchPrivacySettings start account=${ctx.accountId}`);
|
|
927
|
+
try {
|
|
928
|
+
const settings = await withTimeout(
|
|
929
|
+
"fetchPrivacySettings",
|
|
930
|
+
// force=true bypasses Baileys' internal cache on first connect
|
|
931
|
+
sock.fetchPrivacySettings(true),
|
|
932
|
+
queryTimeoutMs
|
|
933
|
+
);
|
|
934
|
+
result.privacySettingsOk = true;
|
|
935
|
+
console.error(
|
|
936
|
+
`${TAG5} fetchPrivacySettings ok account=${ctx.accountId} keys=${settings ? Object.keys(settings).join(",") : ""}`
|
|
937
|
+
);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
const formatted = formatErr(err);
|
|
940
|
+
result.errors.privacySettings = formatted;
|
|
941
|
+
console.error(
|
|
942
|
+
`${TAG5} fetchPrivacySettings FAILED account=${ctx.accountId}: ${formatted}`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
result.elapsedMs = Date.now() - startedAt;
|
|
946
|
+
console.error(
|
|
947
|
+
`${TAG5} runInitQueries done account=${ctx.accountId} blocklistOk=${result.blocklistOk} privacySettingsOk=${result.privacySettingsOk} elapsedMs=${result.elapsedMs}`
|
|
948
|
+
);
|
|
949
|
+
return result;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// app/lib/whatsapp/normalize.ts
|
|
953
|
+
var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
|
|
954
|
+
var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
|
|
955
|
+
function normalizeE164(value) {
|
|
956
|
+
let candidate = value.trim().replace(/[\s\-().]/g, "");
|
|
957
|
+
candidate = candidate.replace(/^whatsapp:/i, "");
|
|
958
|
+
if (candidate.startsWith("+")) candidate = candidate.slice(1);
|
|
959
|
+
else if (candidate.startsWith("00")) candidate = candidate.slice(2);
|
|
960
|
+
if (!/^\d+$/.test(candidate)) return "";
|
|
961
|
+
return candidate;
|
|
962
|
+
}
|
|
963
|
+
function toWhatsappJid(e164) {
|
|
964
|
+
const normalized = normalizeE164(e164);
|
|
965
|
+
return `${normalized}@s.whatsapp.net`;
|
|
966
|
+
}
|
|
967
|
+
function jidToE164(jid) {
|
|
968
|
+
const userMatch = jid.match(WHATSAPP_USER_JID_RE);
|
|
969
|
+
if (userMatch) return userMatch[1];
|
|
970
|
+
const lidMatch = jid.match(WHATSAPP_LID_RE);
|
|
971
|
+
if (lidMatch) return lidMatch[1];
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
function senderJidFromTelephone(senderTelephone) {
|
|
975
|
+
if (!senderTelephone) return null;
|
|
976
|
+
if (isGroupJid(senderTelephone)) return null;
|
|
977
|
+
if (isUserJid(senderTelephone)) return senderTelephone;
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
function isGroupJid(value) {
|
|
981
|
+
const candidate = stripWhatsAppPrefix(value);
|
|
982
|
+
const lower = candidate.toLowerCase();
|
|
983
|
+
if (!lower.endsWith("@g.us")) return false;
|
|
984
|
+
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
|
985
|
+
if (!localPart || localPart.includes("@")) return false;
|
|
986
|
+
return /^[0-9]+(-[0-9]+)*$/.test(localPart);
|
|
987
|
+
}
|
|
988
|
+
function isUserJid(value) {
|
|
989
|
+
const candidate = stripWhatsAppPrefix(value);
|
|
990
|
+
return WHATSAPP_USER_JID_RE.test(candidate) || WHATSAPP_LID_RE.test(candidate);
|
|
991
|
+
}
|
|
992
|
+
function stripWhatsAppPrefix(value) {
|
|
993
|
+
let candidate = value.trim();
|
|
994
|
+
for (; ; ) {
|
|
995
|
+
const before = candidate;
|
|
996
|
+
candidate = candidate.replace(/^whatsapp:/i, "").trim();
|
|
997
|
+
if (candidate === before) return candidate;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function normalizeTarget(value) {
|
|
1001
|
+
const candidate = stripWhatsAppPrefix(value);
|
|
1002
|
+
if (!candidate) return null;
|
|
1003
|
+
if (isGroupJid(candidate)) {
|
|
1004
|
+
const localPart = candidate.slice(0, candidate.length - "@g.us".length);
|
|
1005
|
+
return `${localPart}@g.us`;
|
|
1006
|
+
}
|
|
1007
|
+
if (isUserJid(candidate)) {
|
|
1008
|
+
const phone = jidToE164(candidate);
|
|
1009
|
+
if (!phone) return null;
|
|
1010
|
+
const normalized2 = normalizeE164(phone);
|
|
1011
|
+
return normalized2.length > 1 ? normalized2 : null;
|
|
1012
|
+
}
|
|
1013
|
+
if (candidate.includes("@")) return null;
|
|
1014
|
+
const normalized = normalizeE164(candidate);
|
|
1015
|
+
return normalized.length > 1 ? normalized : null;
|
|
1016
|
+
}
|
|
1017
|
+
function phonesMatch(a, b) {
|
|
1018
|
+
const na = normalizeE164(a);
|
|
1019
|
+
const nb = normalizeE164(b);
|
|
1020
|
+
return na.length > 0 && na === nb;
|
|
1021
|
+
}
|
|
1022
|
+
function isLidJid(jid) {
|
|
1023
|
+
return WHATSAPP_LID_RE.test(jid);
|
|
1024
|
+
}
|
|
1025
|
+
function withSelfLidShortcut(base, selfLid, selfJid) {
|
|
1026
|
+
if (!selfLid || !selfJid) return base;
|
|
1027
|
+
return {
|
|
1028
|
+
getPNForLID: async (lid) => {
|
|
1029
|
+
if (lid === selfLid) return selfJid;
|
|
1030
|
+
try {
|
|
1031
|
+
return await base?.getPNForLID(lid);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.error(`[whatsapp:normalize] base lidMapping.getPNForLID threw lid=${lid}: ${String(err)}`);
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
async function resolveJidToE164(jid, lidMapping) {
|
|
1040
|
+
if (!jid) return null;
|
|
1041
|
+
const userMatch = jid.match(WHATSAPP_USER_JID_RE);
|
|
1042
|
+
if (userMatch) return userMatch[1];
|
|
1043
|
+
if (isLidJid(jid)) {
|
|
1044
|
+
if (lidMapping) {
|
|
1045
|
+
try {
|
|
1046
|
+
const mapped = await lidMapping.getPNForLID(jid);
|
|
1047
|
+
if (mapped) {
|
|
1048
|
+
const phone = jidToE164(mapped);
|
|
1049
|
+
if (phone) {
|
|
1050
|
+
console.error(`[whatsapp:normalize] JID resolved via LID mapping jid=${jid} e164=${phone}`);
|
|
1051
|
+
return phone;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
console.error(`[whatsapp:normalize] LID mapping failed jid=${jid}: ${String(err)}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const lidMatch = jid.match(WHATSAPP_LID_RE);
|
|
1059
|
+
if (lidMatch) {
|
|
1060
|
+
console.error(`[whatsapp:normalize] LID mapping unavailable jid=${jid} using numeric=${lidMatch[1]}`);
|
|
1061
|
+
return lidMatch[1];
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
async function resolveCanonicalConversationJid(remoteJid, lidMapping) {
|
|
1067
|
+
if (!isLidJid(remoteJid)) return { canonical: remoteJid, resolved: "none" };
|
|
1068
|
+
if (lidMapping) {
|
|
1069
|
+
try {
|
|
1070
|
+
const mapped = await lidMapping.getPNForLID(remoteJid);
|
|
1071
|
+
if (mapped) {
|
|
1072
|
+
const digits = jidToE164(mapped);
|
|
1073
|
+
if (digits) return { canonical: `${digits}@s.whatsapp.net`, resolved: "lid2pn" };
|
|
1074
|
+
}
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
console.error(`[whatsapp:normalize] canonical-jid LID mapping failed jid=${remoteJid}: ${String(err)}`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return { canonical: remoteJid, resolved: "none" };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// app/lib/whatsapp/inbound/lid-map.ts
|
|
1083
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1084
|
+
function resolveViaLidMap(map, senderPhone) {
|
|
1085
|
+
const key = normalizeE164(senderPhone);
|
|
1086
|
+
if (!key) return null;
|
|
1087
|
+
const mapped = map[key];
|
|
1088
|
+
return mapped && mapped.length > 0 ? mapped : null;
|
|
1089
|
+
}
|
|
1090
|
+
function lidCaptureFor(senderJid, senderPhone) {
|
|
1091
|
+
if (!isLidJid(senderJid)) return null;
|
|
1092
|
+
const numericLid = jidToE164(senderJid);
|
|
1093
|
+
if (!numericLid) return null;
|
|
1094
|
+
const e164 = normalizeE164(senderPhone);
|
|
1095
|
+
if (!e164 || e164 === numericLid) return null;
|
|
1096
|
+
return { numericLid, e164 };
|
|
1097
|
+
}
|
|
1098
|
+
function readLidMap() {
|
|
1099
|
+
try {
|
|
1100
|
+
if (!existsSync2(LID_MAP_FILE)) return null;
|
|
1101
|
+
const raw = readFileSync2(LID_MAP_FILE, "utf-8").trim();
|
|
1102
|
+
if (!raw) return null;
|
|
1103
|
+
const parsed = JSON.parse(raw);
|
|
1104
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
1105
|
+
return parsed;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function recordLidMapping(numericLid, e164) {
|
|
1111
|
+
try {
|
|
1112
|
+
const key = normalizeE164(numericLid);
|
|
1113
|
+
const val = normalizeE164(e164);
|
|
1114
|
+
if (!key || !val) return;
|
|
1115
|
+
const map = readLidMap() ?? {};
|
|
1116
|
+
if (map[key] === val) return;
|
|
1117
|
+
map[key] = val;
|
|
1118
|
+
writeFileSync2(LID_MAP_FILE, JSON.stringify(map, null, 2));
|
|
1119
|
+
console.error(`[lid-map] recorded lid=${key} userId-phone=present`);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
console.error(`[lid-map] record-failed lid=${numericLid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// app/lib/whatsapp/inbound/extract.ts
|
|
1126
|
+
import {
|
|
1127
|
+
extractMessageContent,
|
|
1128
|
+
getContentType,
|
|
1129
|
+
normalizeMessageContent
|
|
1130
|
+
} from "@whiskeysockets/baileys";
|
|
1131
|
+
function extractQuoted(ctx) {
|
|
1132
|
+
const quoted = ctx?.quotedMessage;
|
|
1133
|
+
if (!quoted) return void 0;
|
|
1134
|
+
const qc = extractMessageContent(quoted);
|
|
1135
|
+
return {
|
|
1136
|
+
id: ctx?.stanzaId ?? "",
|
|
1137
|
+
text: qc?.conversation ?? qc?.extendedTextMessage?.text ?? qc?.imageMessage?.caption ?? void 0,
|
|
1138
|
+
sender: ctx?.participant ?? void 0
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function extractMessage(msg) {
|
|
1142
|
+
const result = {
|
|
1143
|
+
text: "",
|
|
1144
|
+
mentionedJids: [],
|
|
1145
|
+
isLocation: false
|
|
1146
|
+
};
|
|
1147
|
+
if (!msg.message) return result;
|
|
1148
|
+
const content = normalizeMessageContent(msg.message);
|
|
1149
|
+
if (!content) return result;
|
|
1150
|
+
const type = getContentType(content);
|
|
1151
|
+
if (!type) return result;
|
|
1152
|
+
const inner = extractMessageContent(content);
|
|
1153
|
+
if (!inner) return result;
|
|
1154
|
+
if (inner.conversation) {
|
|
1155
|
+
result.text = inner.conversation;
|
|
1156
|
+
} else if (inner.extendedTextMessage) {
|
|
1157
|
+
result.text = inner.extendedTextMessage.text ?? "";
|
|
1158
|
+
result.mentionedJids = inner.extendedTextMessage.contextInfo?.mentionedJid ?? [];
|
|
1159
|
+
result.quotedMessage = extractQuoted(inner.extendedTextMessage.contextInfo);
|
|
1160
|
+
}
|
|
1161
|
+
if (inner.imageMessage) {
|
|
1162
|
+
result.mediaType = "image";
|
|
1163
|
+
result.mimetype = inner.imageMessage.mimetype ?? void 0;
|
|
1164
|
+
result.caption = inner.imageMessage.caption ?? void 0;
|
|
1165
|
+
if (!result.text && result.caption) result.text = result.caption;
|
|
1166
|
+
result.mentionedJids = inner.imageMessage.contextInfo?.mentionedJid ?? result.mentionedJids;
|
|
1167
|
+
result.quotedMessage = extractQuoted(inner.imageMessage.contextInfo);
|
|
1168
|
+
}
|
|
1169
|
+
if (inner.audioMessage) {
|
|
1170
|
+
result.mediaType = "audio";
|
|
1171
|
+
result.mimetype = inner.audioMessage.mimetype ?? void 0;
|
|
1172
|
+
result.quotedMessage = extractQuoted(inner.audioMessage.contextInfo);
|
|
1173
|
+
}
|
|
1174
|
+
if (inner.videoMessage) {
|
|
1175
|
+
result.mediaType = "video";
|
|
1176
|
+
result.mimetype = inner.videoMessage.mimetype ?? void 0;
|
|
1177
|
+
result.caption = inner.videoMessage.caption ?? void 0;
|
|
1178
|
+
if (!result.text && result.caption) result.text = result.caption;
|
|
1179
|
+
result.quotedMessage = extractQuoted(inner.videoMessage.contextInfo);
|
|
1180
|
+
}
|
|
1181
|
+
if (inner.documentMessage) {
|
|
1182
|
+
result.mediaType = "document";
|
|
1183
|
+
result.mimetype = inner.documentMessage.mimetype ?? void 0;
|
|
1184
|
+
result.caption = inner.documentMessage.caption ?? void 0;
|
|
1185
|
+
if (!result.text) {
|
|
1186
|
+
result.text = result.caption ?? `[document: ${inner.documentMessage.fileName ?? "file"}]`;
|
|
1187
|
+
}
|
|
1188
|
+
result.quotedMessage = extractQuoted(inner.documentMessage.contextInfo);
|
|
1189
|
+
}
|
|
1190
|
+
if (inner.stickerMessage) {
|
|
1191
|
+
result.mediaType = "sticker";
|
|
1192
|
+
result.mimetype = inner.stickerMessage.mimetype ?? void 0;
|
|
1193
|
+
if (!result.text) result.text = "[sticker]";
|
|
1194
|
+
}
|
|
1195
|
+
if (inner.locationMessage) {
|
|
1196
|
+
result.isLocation = true;
|
|
1197
|
+
const lat = inner.locationMessage.degreesLatitude;
|
|
1198
|
+
const lon = inner.locationMessage.degreesLongitude;
|
|
1199
|
+
const name = inner.locationMessage.name;
|
|
1200
|
+
const addr = inner.locationMessage.address;
|
|
1201
|
+
const parts = [name, addr, lat && lon ? `${lat},${lon}` : null].filter(Boolean);
|
|
1202
|
+
result.locationText = parts.join(" \u2014 ");
|
|
1203
|
+
if (!result.text) result.text = `[location: ${result.locationText}]`;
|
|
1204
|
+
}
|
|
1205
|
+
if (inner.liveLocationMessage) {
|
|
1206
|
+
result.isLocation = true;
|
|
1207
|
+
const lat = inner.liveLocationMessage.degreesLatitude;
|
|
1208
|
+
const lon = inner.liveLocationMessage.degreesLongitude;
|
|
1209
|
+
result.locationText = `${lat},${lon}`;
|
|
1210
|
+
if (!result.text) result.text = `[live location: ${result.locationText}]`;
|
|
1211
|
+
}
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
function mediaFallbackBody(extracted) {
|
|
1215
|
+
if (extracted.text) return extracted.text;
|
|
1216
|
+
return extracted.mediaType ? `[${extracted.mediaType}]` : "";
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// app/lib/whatsapp/inbound/reaction.ts
|
|
1220
|
+
function toMillis(v) {
|
|
1221
|
+
if (v == null) return null;
|
|
1222
|
+
if (typeof v === "number") return v;
|
|
1223
|
+
if (typeof v.toNumber === "function") return v.toNumber();
|
|
1224
|
+
if (typeof v.low === "number") return (v.low >>> 0) + (v.high ?? 0) * 4294967296;
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
function readReactionEntry(entry) {
|
|
1228
|
+
const targetMsgKeyId = entry.key?.id ?? void 0;
|
|
1229
|
+
if (!targetMsgKeyId) return null;
|
|
1230
|
+
const chatJid = entry.key?.remoteJid ?? entry.reaction?.key?.remoteJid ?? void 0;
|
|
1231
|
+
if (!chatJid) return null;
|
|
1232
|
+
const isGroup = isGroupJid(chatJid);
|
|
1233
|
+
const reactorKey = entry.reaction?.key;
|
|
1234
|
+
const reactorJid = isGroup ? reactorKey?.participant ?? void 0 : reactorKey?.remoteJid ?? chatJid;
|
|
1235
|
+
if (!reactorJid) return null;
|
|
1236
|
+
const emoji = entry.reaction?.text ?? "";
|
|
1237
|
+
return {
|
|
1238
|
+
targetMsgKeyId,
|
|
1239
|
+
chatJid,
|
|
1240
|
+
reactorJid,
|
|
1241
|
+
isGroup,
|
|
1242
|
+
emoji,
|
|
1243
|
+
removed: !emoji,
|
|
1244
|
+
fromMe: !!reactorKey?.fromMe,
|
|
1245
|
+
timestampMs: toMillis(entry.reaction?.senderTimestampMs)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// app/lib/whatsapp/message-store.ts
|
|
1250
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, appendFileSync as appendFileSync2, readdirSync as readdirSync2, statSync } from "fs";
|
|
1251
|
+
import { dirname, join as join2 } from "path";
|
|
1252
|
+
function isReactionRecord(r) {
|
|
1253
|
+
return r.kind === "reaction";
|
|
1254
|
+
}
|
|
1255
|
+
var STORE_ROOT = () => join2(process.env.MAXY_DIR_OVERRIDE ?? MAXY_DIR, "data", "whatsapp-messages");
|
|
1256
|
+
function conversationFile(platformAccountId, remoteJid) {
|
|
1257
|
+
return join2(STORE_ROOT(), platformAccountId, `${remoteJid}.jsonl`);
|
|
1258
|
+
}
|
|
1259
|
+
function listStoredAccountIds() {
|
|
1260
|
+
const root = STORE_ROOT();
|
|
1261
|
+
if (!existsSync3(root)) return [];
|
|
1262
|
+
return readdirSync2(root).filter((name) => {
|
|
1263
|
+
try {
|
|
1264
|
+
return statSync(join2(root, name)).isDirectory();
|
|
1265
|
+
} catch {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
function conversationFilePath(platformAccountId, remoteJid) {
|
|
1271
|
+
return conversationFile(platformAccountId, remoteJid);
|
|
1272
|
+
}
|
|
1273
|
+
function parseLines(raw) {
|
|
1274
|
+
const out = [];
|
|
1275
|
+
for (const line of raw.split("\n")) {
|
|
1276
|
+
if (!line.trim()) continue;
|
|
1277
|
+
try {
|
|
1278
|
+
const rec = JSON.parse(line);
|
|
1279
|
+
if (isReactionRecord(rec)) continue;
|
|
1280
|
+
out.push(rec);
|
|
1281
|
+
} catch {
|
|
1282
|
+
console.error(`[whatsapp-message-store] skip reason=corrupt-line`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
|
|
1288
|
+
function appendMessage(platformAccountId, record, altDedupeRemoteJids = []) {
|
|
1289
|
+
const file = conversationFile(platformAccountId, record.remoteJid);
|
|
1290
|
+
try {
|
|
1291
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1294
|
+
console.error(`[whatsapp-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`);
|
|
1295
|
+
return { created: false };
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const filesToCheck = [file, ...altDedupeRemoteJids.filter((jid) => jid && jid !== record.remoteJid).map((jid) => conversationFile(platformAccountId, jid))];
|
|
1299
|
+
for (const candidate of new Set(filesToCheck)) {
|
|
1300
|
+
if (!existsSync3(candidate)) continue;
|
|
1301
|
+
const existing = parseLines(readFileSync3(candidate, "utf8"));
|
|
1302
|
+
if (existing.some((r) => r.messageId === record.messageId)) {
|
|
1303
|
+
return { created: false };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1308
|
+
console.error(`[whatsapp-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`);
|
|
1309
|
+
return { created: false };
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
appendFileSync2(file, JSON.stringify(record) + "\n");
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1315
|
+
console.error(`[whatsapp-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`);
|
|
1316
|
+
return { created: false };
|
|
1317
|
+
}
|
|
1318
|
+
return { created: true };
|
|
1319
|
+
}
|
|
1320
|
+
function readConversation(platformAccountId, remoteJid) {
|
|
1321
|
+
const file = conversationFile(platformAccountId, remoteJid);
|
|
1322
|
+
try {
|
|
1323
|
+
if (!existsSync3(file)) return [];
|
|
1324
|
+
return parseLines(readFileSync3(file, "utf8")).sort(byDateSentAsc);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1327
|
+
console.error(`[whatsapp-message-store] FAIL op=read reason=${reason.slice(0, 100)} remoteJid=${remoteJid}`);
|
|
1328
|
+
return [];
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function appendReaction(platformAccountId, record) {
|
|
1332
|
+
const file = conversationFile(platformAccountId, record.remoteJid);
|
|
1333
|
+
try {
|
|
1334
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
1335
|
+
appendFileSync2(file, JSON.stringify(record) + "\n");
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1338
|
+
console.error(
|
|
1339
|
+
`[whatsapp-message-store] FAIL op=append-reaction reason=${reason.slice(0, 100)} target=${record.targetMsgKeyId} reactor=${record.reactorTelephone}`
|
|
1340
|
+
);
|
|
1341
|
+
return { appended: false };
|
|
1342
|
+
}
|
|
1343
|
+
return { appended: true };
|
|
1344
|
+
}
|
|
1345
|
+
function readReactions(platformAccountId, remoteJid) {
|
|
1346
|
+
const file = conversationFile(platformAccountId, remoteJid);
|
|
1347
|
+
try {
|
|
1348
|
+
if (!existsSync3(file)) return [];
|
|
1349
|
+
const out = [];
|
|
1350
|
+
for (const line of readFileSync3(file, "utf8").split("\n")) {
|
|
1351
|
+
if (!line.trim()) continue;
|
|
1352
|
+
try {
|
|
1353
|
+
const rec = JSON.parse(line);
|
|
1354
|
+
if (isReactionRecord(rec)) out.push(rec);
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return out;
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1361
|
+
console.error(`[whatsapp-message-store] FAIL op=read-reactions reason=${reason.slice(0, 100)} remoteJid=${remoteJid}`);
|
|
1362
|
+
return [];
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function foldReactions(records) {
|
|
1366
|
+
const latest = /* @__PURE__ */ new Map();
|
|
1367
|
+
const ordered = records.slice().sort((a, b) => a.dateSent.localeCompare(b.dateSent));
|
|
1368
|
+
for (const r of ordered) {
|
|
1369
|
+
let byReactor = latest.get(r.targetMsgKeyId);
|
|
1370
|
+
if (!byReactor) {
|
|
1371
|
+
byReactor = /* @__PURE__ */ new Map();
|
|
1372
|
+
latest.set(r.targetMsgKeyId, byReactor);
|
|
1373
|
+
}
|
|
1374
|
+
byReactor.set(r.reactorTelephone, r);
|
|
1375
|
+
}
|
|
1376
|
+
const out = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const [target, byReactor] of latest) {
|
|
1378
|
+
const surviving = [...byReactor.values()].filter((r) => !r.removed).map((r) => ({ emoji: r.emoji, reactorName: r.reactorName }));
|
|
1379
|
+
if (surviving.length > 0) out.set(target, surviving);
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
1383
|
+
var summaryCache = /* @__PURE__ */ new Map();
|
|
1384
|
+
function summariseRecords(records) {
|
|
1385
|
+
if (records.length === 0) return null;
|
|
1386
|
+
const sorted = records.slice().sort(byDateSentAsc);
|
|
1387
|
+
return {
|
|
1388
|
+
first: sorted[0],
|
|
1389
|
+
last: sorted[sorted.length - 1],
|
|
1390
|
+
firstNamedSender: sorted.find((r) => r.senderName)?.senderName ?? null
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function readConversationSummaries(platformAccountId) {
|
|
1394
|
+
const dir = join2(STORE_ROOT(), platformAccountId);
|
|
1395
|
+
const out = /* @__PURE__ */ new Map();
|
|
1396
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1397
|
+
let fullReads = 0;
|
|
1398
|
+
let cacheHits = 0;
|
|
1399
|
+
let bytesRead = 0;
|
|
1400
|
+
try {
|
|
1401
|
+
if (!existsSync3(dir)) return out;
|
|
1402
|
+
for (const entry of readdirSync2(dir)) {
|
|
1403
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
1404
|
+
const remoteJid = entry.slice(0, -".jsonl".length);
|
|
1405
|
+
const file = join2(dir, entry);
|
|
1406
|
+
seen.add(file);
|
|
1407
|
+
let size;
|
|
1408
|
+
let mtimeMs;
|
|
1409
|
+
try {
|
|
1410
|
+
const st = statSync(file);
|
|
1411
|
+
size = st.size;
|
|
1412
|
+
mtimeMs = st.mtimeMs;
|
|
1413
|
+
} catch {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const cached = summaryCache.get(file);
|
|
1417
|
+
if (cached && cached.size === size && cached.mtimeMs === mtimeMs) {
|
|
1418
|
+
cacheHits++;
|
|
1419
|
+
out.set(remoteJid, cached.summary);
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
let summary;
|
|
1423
|
+
try {
|
|
1424
|
+
const raw = readFileSync3(file, "utf8");
|
|
1425
|
+
bytesRead += Buffer.byteLength(raw);
|
|
1426
|
+
fullReads++;
|
|
1427
|
+
summary = summariseRecords(parseLines(raw));
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1430
|
+
console.error(`[whatsapp-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} remoteJid=${remoteJid}`);
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (!summary) continue;
|
|
1434
|
+
summaryCache.set(file, { size, mtimeMs, summary });
|
|
1435
|
+
out.set(remoteJid, summary);
|
|
1436
|
+
}
|
|
1437
|
+
for (const key of summaryCache.keys()) {
|
|
1438
|
+
if (dirname(key) === dir && !seen.has(key)) summaryCache.delete(key);
|
|
1439
|
+
}
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1442
|
+
console.error(`[whatsapp-message-store] FAIL op=summaries reason=${reason.slice(0, 100)} platformAccountId=${platformAccountId}`);
|
|
1443
|
+
}
|
|
1444
|
+
console.log(
|
|
1445
|
+
`[whatsapp-message-store] op=summaries accountId=${platformAccountId} conversations=${out.size} fullReads=${fullReads} cacheHits=${cacheHits} bytesRead=${bytesRead}`
|
|
1446
|
+
);
|
|
1447
|
+
return out;
|
|
1448
|
+
}
|
|
1449
|
+
function readAllConversations(platformAccountId) {
|
|
1450
|
+
const dir = join2(STORE_ROOT(), platformAccountId);
|
|
1451
|
+
const out = /* @__PURE__ */ new Map();
|
|
1452
|
+
try {
|
|
1453
|
+
if (!existsSync3(dir)) return out;
|
|
1454
|
+
for (const entry of readdirSync2(dir)) {
|
|
1455
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
1456
|
+
const remoteJid = entry.slice(0, -".jsonl".length);
|
|
1457
|
+
out.set(remoteJid, readConversation(platformAccountId, remoteJid));
|
|
1458
|
+
}
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1461
|
+
console.error(`[whatsapp-message-store] FAIL op=read-all reason=${reason.slice(0, 100)} platformAccountId=${platformAccountId}`);
|
|
1462
|
+
}
|
|
1463
|
+
return out;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// app/lib/whatsapp/echo-already-recorded.ts
|
|
1467
|
+
function echoAlreadyRecorded(args) {
|
|
1468
|
+
const messageId = `whatsapp-live:${args.accountId}:${args.remoteJid}:${args.msgKeyId}`;
|
|
1469
|
+
return args.readConversation(args.platformAccountId, args.canonicalRemoteJid).some((r) => r.messageId === messageId);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// app/lib/whatsapp/managed-account.ts
|
|
1473
|
+
function normalizeEntry(v) {
|
|
1474
|
+
return typeof v === "string" ? { account: v, mode: "active" } : { account: v.managesAccount, mode: v.mode ?? "active" };
|
|
1475
|
+
}
|
|
1476
|
+
function managedAccountFor(accountManagers, phone) {
|
|
1477
|
+
for (const [managerPhone, entry] of Object.entries(accountManagers)) {
|
|
1478
|
+
const { account, mode } = normalizeEntry(entry);
|
|
1479
|
+
if (account && phonesMatch(managerPhone, phone)) return { account, mode };
|
|
1480
|
+
}
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
function managerPhonesForAccount(accountManagers, subAccountId) {
|
|
1484
|
+
const phones = [];
|
|
1485
|
+
for (const [managerPhone, entry] of Object.entries(accountManagers)) {
|
|
1486
|
+
const { account } = normalizeEntry(entry);
|
|
1487
|
+
if (account && account === subAccountId) phones.push(managerPhone);
|
|
1488
|
+
}
|
|
1489
|
+
return phones;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// app/lib/whatsapp/inbound/access-control.ts
|
|
1493
|
+
function resolveAccountConfig(config, accountConfig) {
|
|
1494
|
+
return {
|
|
1495
|
+
dmPolicy: accountConfig?.dmPolicy ?? config.dmPolicy ?? "disabled",
|
|
1496
|
+
adminPhones: accountConfig?.adminPhones ?? config.adminPhones ?? [],
|
|
1497
|
+
allowFrom: accountConfig?.allowFrom ?? config.allowFrom ?? [],
|
|
1498
|
+
selfChatMode: accountConfig?.selfChatMode ?? false,
|
|
1499
|
+
// Account-manager bindings are a house-level routing table only — never a
|
|
1500
|
+
// per-account override, so this reads the top-level map unconditionally.
|
|
1501
|
+
accountManagers: config.accountManagers ?? {}
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function isAdminPhone(phone, adminPhones) {
|
|
1505
|
+
const normalized = normalizeE164(phone);
|
|
1506
|
+
if (!normalized) return false;
|
|
1507
|
+
return adminPhones.some((entry) => phonesMatch(entry, normalized));
|
|
1508
|
+
}
|
|
1509
|
+
function checkDmAccess(params) {
|
|
1510
|
+
const { senderPhone, selfPhone, accountId } = params;
|
|
1511
|
+
const cfg = resolveAccountConfig(params.config, params.accountConfig);
|
|
1512
|
+
if (phonesMatch(senderPhone, selfPhone)) {
|
|
1513
|
+
return { allowed: true, reason: "self-phone", agentType: "admin", effectiveAccountId: accountId };
|
|
1514
|
+
}
|
|
1515
|
+
if (isAdminPhone(senderPhone, cfg.adminPhones)) {
|
|
1516
|
+
return { allowed: true, reason: "admin-binding", agentType: "admin", effectiveAccountId: accountId };
|
|
1517
|
+
}
|
|
1518
|
+
const managed = managedAccountFor(cfg.accountManagers, senderPhone);
|
|
1519
|
+
if (managed) {
|
|
1520
|
+
if (params.isValidAccount && !params.isValidAccount(managed.account)) {
|
|
1521
|
+
return { allowed: false, reason: "account-manager-unresolved", agentType: "admin" };
|
|
1522
|
+
}
|
|
1523
|
+
return { allowed: true, reason: "account-manager", agentType: "admin", effectiveAccountId: managed.account, passive: managed.mode === "passive" };
|
|
1524
|
+
}
|
|
1525
|
+
switch (cfg.dmPolicy) {
|
|
1526
|
+
case "open":
|
|
1527
|
+
return { allowed: true, reason: "dm-policy-open", agentType: "public", effectiveAccountId: accountId };
|
|
1528
|
+
case "allowlist": {
|
|
1529
|
+
const inList = cfg.allowFrom.some((entry) => {
|
|
1530
|
+
if (entry === "*") return true;
|
|
1531
|
+
return phonesMatch(entry, senderPhone);
|
|
1532
|
+
});
|
|
1533
|
+
if (inList) {
|
|
1534
|
+
return { allowed: true, reason: "allowlist-match", agentType: "public", effectiveAccountId: accountId };
|
|
1535
|
+
}
|
|
1536
|
+
return { allowed: false, reason: "not-in-allowlist", agentType: "public" };
|
|
1537
|
+
}
|
|
1538
|
+
case "disabled":
|
|
1539
|
+
return { allowed: false, reason: "dm-policy-disabled", agentType: "public" };
|
|
1540
|
+
default:
|
|
1541
|
+
return { allowed: false, reason: "unknown-dm-policy", agentType: "public" };
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
async function resolveDmAccess(params) {
|
|
1545
|
+
const base = checkDmAccess(params);
|
|
1546
|
+
if (!base.allowed || base.agentType !== "public") return base;
|
|
1547
|
+
const e164 = normalizeE164(params.senderPhone);
|
|
1548
|
+
if (!e164 || !params.accountId) return base;
|
|
1549
|
+
try {
|
|
1550
|
+
const personId = await params.resolvePersonByPhone(params.accountId, e164);
|
|
1551
|
+
return { ...base, personId };
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
console.error(
|
|
1554
|
+
`[whatsapp:route] op=person-resolve-error senderId=${e164} err=${err instanceof Error ? err.message : String(err)}`
|
|
1555
|
+
);
|
|
1556
|
+
return base;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function checkGroupAccess(_params) {
|
|
1560
|
+
return { allowed: false, reason: "group-observe-only", agentType: "public" };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// app/lib/whatsapp/inbound/dispatch-mode.ts
|
|
1564
|
+
function shouldDispatchInbound(config, accountId) {
|
|
1565
|
+
return config.accounts?.[accountId]?.dispatchInbound ?? true;
|
|
1566
|
+
}
|
|
1567
|
+
function decideDispatch(conn, payload, deps) {
|
|
1568
|
+
if (shouldDispatchInbound(deps.config, conn.accountId)) {
|
|
1569
|
+
deps.emit(payload);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
deps.log(`[whatsapp:observe] op=dispatch-suppressed account=${conn.accountId} sender=${payload.senderPhone} agentType=${payload.agentType}`);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// app/lib/whatsapp/inbound/notify-frame.ts
|
|
1576
|
+
function decideWaNotify(input) {
|
|
1577
|
+
if (input.upsertType !== "notify") return { emit: false, reason: "append" };
|
|
1578
|
+
if (input.fromMe) return { emit: false, reason: "from-me" };
|
|
1579
|
+
if (!input.hasPersistableContent) return { emit: false, reason: "no-content" };
|
|
1580
|
+
if (!input.msgKeyId) return { emit: false, reason: "no-id" };
|
|
1581
|
+
if (!input.access) return { emit: false, reason: "group" };
|
|
1582
|
+
if (!input.access.allowed) return { emit: false, reason: "not-allowed" };
|
|
1583
|
+
if (input.isDuplicate()) return { emit: false, reason: "duplicate" };
|
|
1584
|
+
return { emit: true, senderType: classify(input.access) };
|
|
1585
|
+
}
|
|
1586
|
+
function classify(access) {
|
|
1587
|
+
if (access.reason === "account-manager") {
|
|
1588
|
+
return access.passive === true ? "manager-passive" : "manager-active";
|
|
1589
|
+
}
|
|
1590
|
+
return access.agentType === "admin" ? "admin" : "public";
|
|
1591
|
+
}
|
|
1592
|
+
function formatNotifyBody(text, mediaType) {
|
|
1593
|
+
const source = text.length > 0 ? text : mediaType ?? "";
|
|
1594
|
+
if (source.length === 0) return { body: "", truncated: false };
|
|
1595
|
+
const lines = source.split("\n");
|
|
1596
|
+
if (lines.length <= 2) return { body: source, truncated: false };
|
|
1597
|
+
return { body: lines.slice(0, 2).join("\n"), truncated: true };
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// app/lib/whatsapp/inbound/notify-dedupe.ts
|
|
1601
|
+
var NOTIFY_TTL_MS = 20 * 6e4;
|
|
1602
|
+
var NOTIFY_MAX = 5e3;
|
|
1603
|
+
var notifyCache = /* @__PURE__ */ new Map();
|
|
1604
|
+
function isNotifyDuplicate(msgKeyId) {
|
|
1605
|
+
const now = Date.now();
|
|
1606
|
+
const entry = notifyCache.get(msgKeyId);
|
|
1607
|
+
if (entry && now - entry.ts <= NOTIFY_TTL_MS) return true;
|
|
1608
|
+
for (const [key, e] of notifyCache) {
|
|
1609
|
+
if (now - e.ts > NOTIFY_TTL_MS) notifyCache.delete(key);
|
|
1610
|
+
}
|
|
1611
|
+
if (notifyCache.size >= NOTIFY_MAX) {
|
|
1612
|
+
const excess = notifyCache.size - NOTIFY_MAX + 1;
|
|
1613
|
+
let dropped = 0;
|
|
1614
|
+
for (const key of notifyCache.keys()) {
|
|
1615
|
+
if (dropped >= excess) break;
|
|
1616
|
+
notifyCache.delete(key);
|
|
1617
|
+
dropped++;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
notifyCache.set(msgKeyId, { ts: now });
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// app/lib/reader/format-phone.ts
|
|
1625
|
+
import { parsePhoneNumberFromString } from "libphonenumber-js";
|
|
1626
|
+
function formatSenderPhone(senderId) {
|
|
1627
|
+
const digits = normalizeE164(senderId);
|
|
1628
|
+
if (!digits) return senderId;
|
|
1629
|
+
const parsed = parsePhoneNumberFromString(`+${digits}`);
|
|
1630
|
+
return parsed?.isPossible() ? parsed.formatInternational() : senderId;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// app/lib/access-gate.ts
|
|
1634
|
+
import neo4j from "neo4j-driver";
|
|
1635
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1636
|
+
import { resolve } from "path";
|
|
1637
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1638
|
+
var PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT ?? resolve(process.cwd(), "..");
|
|
1639
|
+
var driver = null;
|
|
1640
|
+
function readPassword() {
|
|
1641
|
+
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
1642
|
+
const passwordFile = resolve(PLATFORM_ROOT, "config/.neo4j-password");
|
|
1643
|
+
try {
|
|
1644
|
+
return readFileSync4(passwordFile, "utf-8").trim();
|
|
1645
|
+
} catch {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function getDriver() {
|
|
1652
|
+
if (!driver) {
|
|
1653
|
+
const uri = process.env.NEO4J_URI;
|
|
1654
|
+
if (!uri) {
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
"[ui/access-gate] NEO4J_URI unset \u2014 refusing to default to bolt://localhost:7687"
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const user = process.env.NEO4J_USER ?? "neo4j";
|
|
1660
|
+
const password = readPassword();
|
|
1661
|
+
console.error(`[ui/access-gate] resolved neo4j_uri=${uri}`);
|
|
1662
|
+
driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
|
|
1663
|
+
maxConnectionPoolSize: 5
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return driver;
|
|
1667
|
+
}
|
|
1668
|
+
var sessionFactory = null;
|
|
1669
|
+
function getSession2() {
|
|
1670
|
+
return sessionFactory ? sessionFactory() : getDriver().session();
|
|
1671
|
+
}
|
|
1672
|
+
process.on("SIGINT", async () => {
|
|
1673
|
+
if (driver) {
|
|
1674
|
+
await driver.close();
|
|
1675
|
+
driver = null;
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
var rateLimitMap = /* @__PURE__ */ new Map();
|
|
1679
|
+
var ACCESS_MAX_ATTEMPTS = 5;
|
|
1680
|
+
var ACCESS_LOCKOUT_MS = 15 * 60 * 1e3;
|
|
1681
|
+
var ACCESS_WINDOW_MS = 15 * 60 * 1e3;
|
|
1682
|
+
function checkAccessRateLimit(ip, agentSlug) {
|
|
1683
|
+
const key = `${ip}:${agentSlug}`;
|
|
1684
|
+
const entry = rateLimitMap.get(key);
|
|
1685
|
+
if (!entry) return null;
|
|
1686
|
+
const now = Date.now();
|
|
1687
|
+
if (entry.lockedUntil && now < entry.lockedUntil) {
|
|
1688
|
+
const remainingMs = entry.lockedUntil - now;
|
|
1689
|
+
const remainingS = Math.ceil(remainingMs / 1e3);
|
|
1690
|
+
return `Too many attempts. Try again in ${remainingS}s`;
|
|
1691
|
+
}
|
|
1692
|
+
if (now - entry.firstAttempt > ACCESS_WINDOW_MS) {
|
|
1693
|
+
rateLimitMap.delete(key);
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
function recordAccessFailedAttempt(ip, agentSlug) {
|
|
1699
|
+
const key = `${ip}:${agentSlug}`;
|
|
1700
|
+
const now = Date.now();
|
|
1701
|
+
const entry = rateLimitMap.get(key);
|
|
1702
|
+
if (!entry || now - entry.firstAttempt > ACCESS_WINDOW_MS) {
|
|
1703
|
+
rateLimitMap.set(key, { attempts: 1, firstAttempt: now });
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
entry.attempts++;
|
|
1707
|
+
if (entry.attempts >= ACCESS_MAX_ATTEMPTS) {
|
|
1708
|
+
entry.lockedUntil = now + ACCESS_LOCKOUT_MS;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function clearAccessRateLimit(ip, agentSlug) {
|
|
1712
|
+
rateLimitMap.delete(`${ip}:${agentSlug}`);
|
|
1713
|
+
}
|
|
1714
|
+
var contactRateMap = /* @__PURE__ */ new Map();
|
|
1715
|
+
var REQUEST_LINK_MAX = 3;
|
|
1716
|
+
var REQUEST_LINK_WINDOW_MS = 60 * 60 * 1e3;
|
|
1717
|
+
function checkRequestLinkRateLimit(contactValue) {
|
|
1718
|
+
const entry = contactRateMap.get(contactValue);
|
|
1719
|
+
if (!entry) return null;
|
|
1720
|
+
const now = Date.now();
|
|
1721
|
+
if (now - entry.windowStart > REQUEST_LINK_WINDOW_MS) {
|
|
1722
|
+
contactRateMap.delete(contactValue);
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
if (entry.count >= REQUEST_LINK_MAX) {
|
|
1726
|
+
const remainingMs = REQUEST_LINK_WINDOW_MS - (now - entry.windowStart);
|
|
1727
|
+
const remainingM = Math.ceil(remainingMs / 6e4);
|
|
1728
|
+
return `Too many requests. Try again in ${remainingM} minute(s)`;
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
function recordRequestLinkAttempt(contactValue) {
|
|
1733
|
+
const now = Date.now();
|
|
1734
|
+
const entry = contactRateMap.get(contactValue);
|
|
1735
|
+
if (!entry || now - entry.windowStart > REQUEST_LINK_WINDOW_MS) {
|
|
1736
|
+
contactRateMap.set(contactValue, { count: 1, windowStart: now });
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
entry.count++;
|
|
1740
|
+
}
|
|
1741
|
+
function maskContact(value) {
|
|
1742
|
+
if (value.includes("@")) {
|
|
1743
|
+
const [local, domain] = value.split("@");
|
|
1744
|
+
return `${local[0]}***@${domain}`;
|
|
1745
|
+
}
|
|
1746
|
+
return "****";
|
|
1747
|
+
}
|
|
1748
|
+
function readGrant(record) {
|
|
1749
|
+
const g = record.get("g").properties;
|
|
1750
|
+
const personId = record.get("personId");
|
|
1751
|
+
return {
|
|
1752
|
+
grantId: record.get("grantId"),
|
|
1753
|
+
agentSlug: String(g.agentSlug),
|
|
1754
|
+
accountId: String(g.accountId),
|
|
1755
|
+
contactValue: String(g.contactValue),
|
|
1756
|
+
displayName: record.get("givenName") ?? null,
|
|
1757
|
+
personId,
|
|
1758
|
+
expiresAt: g.expiresAt ? new Date(String(g.expiresAt)).getTime() : null,
|
|
1759
|
+
status: String(g.status),
|
|
1760
|
+
sliceToken: String(g.sliceToken ?? "")
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
async function findGrantByMagicToken(token) {
|
|
1764
|
+
const session = getSession2();
|
|
1765
|
+
try {
|
|
1766
|
+
const result = await session.run(
|
|
1767
|
+
`MATCH (p:Person)-[:HAS_ACCESS]->(g:AccessGrant {magicToken: $token})
|
|
1768
|
+
RETURN g, p.givenName AS givenName,
|
|
1769
|
+
elementId(g) AS grantId,
|
|
1770
|
+
elementId(p) AS personId`,
|
|
1771
|
+
{ token }
|
|
1772
|
+
);
|
|
1773
|
+
if (result.records.length === 0) return null;
|
|
1774
|
+
return readGrant(result.records[0]);
|
|
1775
|
+
} finally {
|
|
1776
|
+
await session.close();
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
|
|
1780
|
+
const session = getSession2();
|
|
1781
|
+
try {
|
|
1782
|
+
const result = await session.run(
|
|
1783
|
+
`MATCH (p:Person)-[:HAS_ACCESS]->(g:AccessGrant {
|
|
1784
|
+
contactValue: $contactValue,
|
|
1785
|
+
agentSlug: $agentSlug,
|
|
1786
|
+
accountId: $accountId
|
|
1787
|
+
})
|
|
1788
|
+
WHERE g.status IN ['invited', 'active']
|
|
1789
|
+
RETURN g, p.givenName AS givenName,
|
|
1790
|
+
elementId(g) AS grantId,
|
|
1791
|
+
elementId(p) AS personId`,
|
|
1792
|
+
{ contactValue, agentSlug, accountId }
|
|
1793
|
+
);
|
|
1794
|
+
if (result.records.length === 0) return null;
|
|
1795
|
+
return readGrant(result.records[0]);
|
|
1796
|
+
} finally {
|
|
1797
|
+
await session.close();
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async function findGrantByContact(contactValue, agentSlug, accountId) {
|
|
1801
|
+
const session = getSession2();
|
|
1802
|
+
try {
|
|
1803
|
+
const result = await session.run(
|
|
1804
|
+
`MATCH (p:Person)-[:HAS_ACCESS]->(g:AccessGrant {
|
|
1805
|
+
contactValue: $contactValue,
|
|
1806
|
+
agentSlug: $agentSlug,
|
|
1807
|
+
accountId: $accountId
|
|
1808
|
+
})
|
|
1809
|
+
RETURN g, p.givenName AS givenName,
|
|
1810
|
+
elementId(g) AS grantId,
|
|
1811
|
+
elementId(p) AS personId
|
|
1812
|
+
LIMIT 1`,
|
|
1813
|
+
{ contactValue, agentSlug, accountId }
|
|
1814
|
+
);
|
|
1815
|
+
if (result.records.length === 0) return null;
|
|
1816
|
+
return readGrant(result.records[0]);
|
|
1817
|
+
} finally {
|
|
1818
|
+
await session.close();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async function resolvePersonByPhone(accountId, phone) {
|
|
1822
|
+
const session = getSession2();
|
|
1823
|
+
try {
|
|
1824
|
+
const result = await session.run(
|
|
1825
|
+
`MATCH (p:Person {accountId: $accountId, phone: $phone})
|
|
1826
|
+
RETURN elementId(p) AS personId
|
|
1827
|
+
LIMIT 1`,
|
|
1828
|
+
{ accountId, phone }
|
|
1829
|
+
);
|
|
1830
|
+
if (result.records.length === 0) return null;
|
|
1831
|
+
return result.records[0].get("personId");
|
|
1832
|
+
} finally {
|
|
1833
|
+
await session.close();
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
async function ensureManagerPerson(accountId, phone) {
|
|
1837
|
+
const session = getSession2();
|
|
1838
|
+
const normalized = normalizeE164(phone) || phone;
|
|
1839
|
+
try {
|
|
1840
|
+
const result = await session.run(
|
|
1841
|
+
`MERGE (p:Person {accountId: $accountId, phone: $phone})
|
|
1842
|
+
ON CREATE SET p.source = 'wa-passive-manager'
|
|
1843
|
+
RETURN elementId(p) AS personId`,
|
|
1844
|
+
{ accountId, phone: normalized }
|
|
1845
|
+
);
|
|
1846
|
+
if (result.records.length === 0) return null;
|
|
1847
|
+
return result.records[0].get("personId");
|
|
1848
|
+
} finally {
|
|
1849
|
+
await session.close();
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
async function enrolPerson(input) {
|
|
1853
|
+
const session = getSession2();
|
|
1854
|
+
const sliceToken = randomUUID2();
|
|
1855
|
+
const phone = normalizeE164(input.phone) || input.phone;
|
|
1856
|
+
try {
|
|
1857
|
+
const result = await session.run(
|
|
1858
|
+
`MERGE (p:Person {accountId: $accountId, phone: $phone})
|
|
1859
|
+
MERGE (p)-[:HAS_ACCESS]->(g:AccessGrant {
|
|
1860
|
+
accountId: $accountId, agentSlug: $agentSlug, contactValue: $email
|
|
1861
|
+
})
|
|
1862
|
+
ON CREATE SET g.status = 'invited', g.sliceToken = $sliceToken
|
|
1863
|
+
RETURN elementId(p) AS personId`,
|
|
1864
|
+
{
|
|
1865
|
+
accountId: input.accountId,
|
|
1866
|
+
phone,
|
|
1867
|
+
email: input.email,
|
|
1868
|
+
agentSlug: input.agentSlug,
|
|
1869
|
+
sliceToken
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
1872
|
+
return result.records[0].get("personId");
|
|
1873
|
+
} finally {
|
|
1874
|
+
await session.close();
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async function consumeMagicTokenAndActivate(grantId) {
|
|
1878
|
+
const session = getSession2();
|
|
1879
|
+
try {
|
|
1880
|
+
const result = await session.run(
|
|
1881
|
+
`MATCH (g:AccessGrant) WHERE elementId(g) = $grantId
|
|
1882
|
+
SET g.magicToken = null,
|
|
1883
|
+
g.magicTokenExpiresAt = null,
|
|
1884
|
+
g.status = "active"
|
|
1885
|
+
RETURN count(g) AS affected`,
|
|
1886
|
+
{ grantId }
|
|
1887
|
+
);
|
|
1888
|
+
const affected = result.records[0]?.get("affected");
|
|
1889
|
+
const count = typeof affected === "number" ? affected : affected?.toNumber?.() ?? 0;
|
|
1890
|
+
if (count === 0) {
|
|
1891
|
+
throw new Error(`[access-gate] consumeMagicTokenAndActivate: no grant matched grantId=${grantId}`);
|
|
1892
|
+
}
|
|
1893
|
+
} finally {
|
|
1894
|
+
await session.close();
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
async function generateNewMagicToken(grantId) {
|
|
1898
|
+
const token = randomUUID2();
|
|
1899
|
+
const session = getSession2();
|
|
1900
|
+
try {
|
|
1901
|
+
const result = await session.run(
|
|
1902
|
+
`MATCH (g:AccessGrant) WHERE elementId(g) = $grantId
|
|
1903
|
+
SET g.magicToken = $token,
|
|
1904
|
+
g.magicTokenExpiresAt = datetime() + duration('PT15M')
|
|
1905
|
+
RETURN count(g) AS affected`,
|
|
1906
|
+
{ grantId, token }
|
|
1907
|
+
);
|
|
1908
|
+
const affected = result.records[0]?.get("affected");
|
|
1909
|
+
const count = typeof affected === "number" ? affected : affected?.toNumber?.() ?? 0;
|
|
1910
|
+
if (count === 0) {
|
|
1911
|
+
throw new Error(`[access-gate] generateNewMagicToken: no grant matched grantId=${grantId}`);
|
|
1912
|
+
}
|
|
1913
|
+
} finally {
|
|
1914
|
+
await session.close();
|
|
1915
|
+
}
|
|
1916
|
+
return token;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// app/lib/whatsapp/inbound/dedupe.ts
|
|
1920
|
+
var AGENT_SENT_TTL_MS = 2 * 6e4;
|
|
1921
|
+
var AGENT_SENT_MAX = 500;
|
|
1922
|
+
var INBOUND_TTL_MS = 20 * 6e4;
|
|
1923
|
+
var INBOUND_MAX = 5e3;
|
|
1924
|
+
var agentSentCache = /* @__PURE__ */ new Map();
|
|
1925
|
+
var inboundCache = /* @__PURE__ */ new Map();
|
|
1926
|
+
function pruneCache(cache3, ttl, max) {
|
|
1927
|
+
const now = Date.now();
|
|
1928
|
+
for (const [key, entry] of cache3) {
|
|
1929
|
+
if (now - entry.ts > ttl) cache3.delete(key);
|
|
1930
|
+
}
|
|
1931
|
+
if (cache3.size > max) {
|
|
1932
|
+
const excess = cache3.size - max;
|
|
1933
|
+
let count = 0;
|
|
1934
|
+
for (const key of cache3.keys()) {
|
|
1935
|
+
if (count >= excess) break;
|
|
1936
|
+
cache3.delete(key);
|
|
1937
|
+
count++;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function trackAgentSentMessage(messageId) {
|
|
1942
|
+
pruneCache(agentSentCache, AGENT_SENT_TTL_MS, AGENT_SENT_MAX);
|
|
1943
|
+
agentSentCache.set(messageId, { ts: Date.now() });
|
|
1944
|
+
}
|
|
1945
|
+
function isAgentSentMessage(messageId) {
|
|
1946
|
+
const entry = agentSentCache.get(messageId);
|
|
1947
|
+
if (!entry) return false;
|
|
1948
|
+
if (Date.now() - entry.ts > AGENT_SENT_TTL_MS) {
|
|
1949
|
+
agentSentCache.delete(messageId);
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
function isDuplicateInbound(key) {
|
|
1955
|
+
const entry = inboundCache.get(key);
|
|
1956
|
+
if (!entry) {
|
|
1957
|
+
pruneCache(inboundCache, INBOUND_TTL_MS, INBOUND_MAX);
|
|
1958
|
+
inboundCache.set(key, { ts: Date.now() });
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
if (Date.now() - entry.ts > INBOUND_TTL_MS) {
|
|
1962
|
+
inboundCache.delete(key);
|
|
1963
|
+
inboundCache.set(key, { ts: Date.now() });
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// app/lib/whatsapp/inbound/self-chat.ts
|
|
1970
|
+
var TAG6 = "[whatsapp:self-chat]";
|
|
1971
|
+
function normalizeJid(jid) {
|
|
1972
|
+
const m = jid.match(/^(\d+)(?::\d+)?@(s\.whatsapp\.net|lid)$/i);
|
|
1973
|
+
return m ? `${m[1]}@${m[2].toLowerCase()}` : jid;
|
|
1974
|
+
}
|
|
1975
|
+
function isSelfChatJid(remoteJid, self) {
|
|
1976
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1977
|
+
if (self.selfJid) candidates.add(normalizeJid(self.selfJid));
|
|
1978
|
+
if (self.selfLid) candidates.add(normalizeJid(self.selfLid));
|
|
1979
|
+
if (self.selfPhone) candidates.add(`${self.selfPhone}@s.whatsapp.net`);
|
|
1980
|
+
if (candidates.size === 0) return false;
|
|
1981
|
+
return candidates.has(normalizeJid(remoteJid));
|
|
1982
|
+
}
|
|
1983
|
+
function evaluateSelfChatCommand(input) {
|
|
1984
|
+
if (!input.fromMe) return { kind: "not-self-chat" };
|
|
1985
|
+
if (!isSelfChatJid(input.remoteJid, input)) return { kind: "not-self-chat" };
|
|
1986
|
+
const tsMs = input.messageTimestampSec ? input.messageTimestampSec * 1e3 : 0;
|
|
1987
|
+
const connAtMs = input.lastConnectedAtMs ?? 0;
|
|
1988
|
+
const msgId = input.msgId ?? "?";
|
|
1989
|
+
console.error(
|
|
1990
|
+
`${TAG6} op=seen account=${input.accountId} msgId=${msgId} tsMs=${tsMs} connAtMs=${connAtMs} fromMe=true`
|
|
1991
|
+
);
|
|
1992
|
+
const skip = (reason) => {
|
|
1993
|
+
console.error(`${TAG6} op=skip account=${input.accountId} msgId=${msgId} reason=${reason}`);
|
|
1994
|
+
return { kind: "skip", reason };
|
|
1995
|
+
};
|
|
1996
|
+
if (input.msgId && isAgentSentMessage(input.msgId)) return skip("echo");
|
|
1997
|
+
if (!(tsMs > connAtMs)) return skip("backfill");
|
|
1998
|
+
if (isDuplicateInbound(`${input.accountId}:${input.remoteJid}:${input.msgId}`)) return skip("duplicate");
|
|
1999
|
+
if (!input.text) return skip("no-text");
|
|
2000
|
+
console.error(
|
|
2001
|
+
`${TAG6} op=dispatch account=${input.accountId} msgId=${msgId} bytes=${Buffer.byteLength(input.text, "utf8")} agentType=admin`
|
|
2002
|
+
);
|
|
2003
|
+
return { kind: "dispatch", text: input.text };
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// app/lib/whatsapp/activity.ts
|
|
2007
|
+
var TAG7 = "[whatsapp:activity]";
|
|
2008
|
+
var RECENT_EVENTS_MAX = 200;
|
|
2009
|
+
var counters = /* @__PURE__ */ new Map();
|
|
2010
|
+
var recentEvents = [];
|
|
2011
|
+
function recordActivity(event) {
|
|
2012
|
+
try {
|
|
2013
|
+
const now = Date.now();
|
|
2014
|
+
const { accountId, direction, jid, messageType } = event;
|
|
2015
|
+
let acct = counters.get(accountId);
|
|
2016
|
+
if (!acct) {
|
|
2017
|
+
acct = { inbound: 0, outbound: 0, firstEventAt: now, lastEventAt: now };
|
|
2018
|
+
counters.set(accountId, acct);
|
|
2019
|
+
}
|
|
2020
|
+
acct[direction]++;
|
|
2021
|
+
acct.lastEventAt = now;
|
|
2022
|
+
const entry = {
|
|
2023
|
+
channel: "whatsapp",
|
|
2024
|
+
accountId,
|
|
2025
|
+
direction,
|
|
2026
|
+
jid,
|
|
2027
|
+
timestamp: now,
|
|
2028
|
+
messageType
|
|
2029
|
+
};
|
|
2030
|
+
recentEvents.push(entry);
|
|
2031
|
+
if (recentEvents.length > RECENT_EVENTS_MAX) {
|
|
2032
|
+
recentEvents.splice(0, recentEvents.length - RECENT_EVENTS_MAX);
|
|
2033
|
+
}
|
|
2034
|
+
console.error(
|
|
2035
|
+
`${TAG7} channel-activity: direction=${direction} account=${accountId} jid=${jid}` + (messageType ? ` type=${messageType}` : "")
|
|
2036
|
+
);
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
console.error(`${TAG7} recording failed: ${String(err)}`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
function getChannelActivity(accountId) {
|
|
2042
|
+
const accounts = [];
|
|
2043
|
+
for (const [id, acct] of counters) {
|
|
2044
|
+
if (accountId && id !== accountId) continue;
|
|
2045
|
+
accounts.push({
|
|
2046
|
+
accountId: id,
|
|
2047
|
+
inbound: acct.inbound,
|
|
2048
|
+
outbound: acct.outbound,
|
|
2049
|
+
total: acct.inbound + acct.outbound,
|
|
2050
|
+
firstEventAt: acct.firstEventAt,
|
|
2051
|
+
lastEventAt: acct.lastEventAt
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
const events = accountId ? recentEvents.filter((e) => e.accountId === accountId) : [...recentEvents];
|
|
2055
|
+
return { accounts, recentEvents: events };
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// app/lib/whatsapp/outbound/send.ts
|
|
2059
|
+
var TAG8 = "[whatsapp:outbound]";
|
|
2060
|
+
function buildQuoted(q) {
|
|
2061
|
+
return {
|
|
2062
|
+
key: {
|
|
2063
|
+
remoteJid: q.remoteJid,
|
|
2064
|
+
id: q.id,
|
|
2065
|
+
fromMe: q.fromMe,
|
|
2066
|
+
...q.participant ? { participant: q.participant } : {}
|
|
2067
|
+
},
|
|
2068
|
+
message: { conversation: q.body ?? "" }
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
async function sendTextMessage(sock, to, text, opts) {
|
|
2072
|
+
try {
|
|
2073
|
+
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
2074
|
+
const payload = { text };
|
|
2075
|
+
const options = opts?.quoted ? { quoted: buildQuoted(opts.quoted) } : void 0;
|
|
2076
|
+
const result = await sock.sendMessage(jid, payload, options);
|
|
2077
|
+
const messageId = result?.key?.id;
|
|
2078
|
+
if (messageId) {
|
|
2079
|
+
trackAgentSentMessage(messageId);
|
|
2080
|
+
console.error(`${TAG8} sent text to=${jid} id=${messageId}`);
|
|
2081
|
+
}
|
|
2082
|
+
if (opts?.accountId) {
|
|
2083
|
+
recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: "text" });
|
|
2084
|
+
}
|
|
2085
|
+
return { success: true, messageId: messageId ?? void 0 };
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
console.error(`${TAG8} send failed to=${to}: ${String(err)}`);
|
|
2088
|
+
return { success: false, error: String(err) };
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async function sendComposing(sock, to) {
|
|
2092
|
+
try {
|
|
2093
|
+
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
2094
|
+
await sock.sendPresenceUpdate("composing", jid);
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
async function sendPaused(sock, to) {
|
|
2099
|
+
try {
|
|
2100
|
+
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
2101
|
+
await sock.sendPresenceUpdate("paused", jid);
|
|
2102
|
+
} catch {
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async function sendReadReceipt(sock, chatJid, messageIds, participant, accountId) {
|
|
2106
|
+
const idTag = messageIds[0] ?? "?";
|
|
2107
|
+
try {
|
|
2108
|
+
await sock.readMessages([
|
|
2109
|
+
...messageIds.map((id) => ({
|
|
2110
|
+
remoteJid: chatJid,
|
|
2111
|
+
id,
|
|
2112
|
+
participant
|
|
2113
|
+
}))
|
|
2114
|
+
]);
|
|
2115
|
+
console.error(
|
|
2116
|
+
`[whatsapp:receipt] op=ack account=${accountId ?? "?"} jid=${chatJid} id=${idTag} ok=true`
|
|
2117
|
+
);
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
console.error(
|
|
2120
|
+
`[whatsapp:receipt] op=ack account=${accountId ?? "?"} jid=${chatJid} id=${idTag} ok=false reason=${String(err).slice(0, 120)}`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
async function sendMediaMessage(sock, to, media, opts) {
|
|
2125
|
+
try {
|
|
2126
|
+
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
2127
|
+
let payload;
|
|
2128
|
+
switch (media.type) {
|
|
2129
|
+
case "image":
|
|
2130
|
+
payload = {
|
|
2131
|
+
image: media.buffer,
|
|
2132
|
+
mimetype: media.mimetype,
|
|
2133
|
+
caption: media.caption
|
|
2134
|
+
};
|
|
2135
|
+
break;
|
|
2136
|
+
case "audio":
|
|
2137
|
+
payload = {
|
|
2138
|
+
audio: media.buffer,
|
|
2139
|
+
mimetype: media.mimetype,
|
|
2140
|
+
ptt: media.ptt ?? false
|
|
2141
|
+
};
|
|
2142
|
+
break;
|
|
2143
|
+
case "video":
|
|
2144
|
+
payload = {
|
|
2145
|
+
video: media.buffer,
|
|
2146
|
+
mimetype: media.mimetype,
|
|
2147
|
+
caption: media.caption
|
|
2148
|
+
};
|
|
2149
|
+
break;
|
|
2150
|
+
case "document":
|
|
2151
|
+
payload = {
|
|
2152
|
+
document: media.buffer,
|
|
2153
|
+
mimetype: media.mimetype,
|
|
2154
|
+
fileName: media.filename ?? "file",
|
|
2155
|
+
caption: media.caption
|
|
2156
|
+
};
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
const options = opts?.quoted ? { quoted: buildQuoted(opts.quoted) } : void 0;
|
|
2160
|
+
const result = await sock.sendMessage(jid, payload, options);
|
|
2161
|
+
const messageId = result?.key?.id;
|
|
2162
|
+
if (messageId) {
|
|
2163
|
+
trackAgentSentMessage(messageId);
|
|
2164
|
+
console.error(`${TAG8} sent ${media.type} to=${jid} id=${messageId}`);
|
|
2165
|
+
}
|
|
2166
|
+
if (opts?.accountId) {
|
|
2167
|
+
recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: media.type });
|
|
2168
|
+
}
|
|
2169
|
+
return { success: true, messageId: messageId ?? void 0 };
|
|
2170
|
+
} catch (err) {
|
|
2171
|
+
console.error(`${TAG8} send media failed to=${to}: ${String(err)}`);
|
|
2172
|
+
return { success: false, error: String(err) };
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// app/lib/whatsapp/persist-message.ts
|
|
2177
|
+
var TAG9 = "[whatsapp-persist]";
|
|
2178
|
+
var sessionWriteLocks = /* @__PURE__ */ new Map();
|
|
2179
|
+
async function persistWhatsAppMessage(input) {
|
|
2180
|
+
if (!input.body && !input.attachmentId || !input.msgKeyId) return null;
|
|
2181
|
+
const messageId = `whatsapp-live:${input.accountId}:${input.remoteJid}:${input.msgKeyId}`;
|
|
2182
|
+
const canonicalRemoteJid = input.canonicalRemoteJid;
|
|
2183
|
+
const keyResolved = canonicalRemoteJid !== input.remoteJid ? "lid2pn" : "none";
|
|
2184
|
+
console.error(
|
|
2185
|
+
`[whatsapp-message-store] op=key raw=${input.remoteJid} canonical=${canonicalRemoteJid} resolved=${keyResolved}`
|
|
2186
|
+
);
|
|
2187
|
+
const senderTelephone = input.fromMe && input.selfPhone ? input.selfPhone : input.senderPhone;
|
|
2188
|
+
const dateSentIso = new Date(input.timestamp * 1e3).toISOString();
|
|
2189
|
+
const scope = deriveScope(input.cacheKey);
|
|
2190
|
+
const { givenName, familyName } = splitName(input.pushName);
|
|
2191
|
+
const prev = sessionWriteLocks.get(input.cacheKey);
|
|
2192
|
+
let release;
|
|
2193
|
+
const mine = new Promise((resolve6) => {
|
|
2194
|
+
release = resolve6;
|
|
2195
|
+
});
|
|
2196
|
+
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
2197
|
+
sessionWriteLocks.set(input.cacheKey, chained);
|
|
2198
|
+
await prev;
|
|
2199
|
+
const t0 = Date.now();
|
|
2200
|
+
let session = null;
|
|
2201
|
+
try {
|
|
2202
|
+
session = getSession();
|
|
2203
|
+
const cypher = `
|
|
2204
|
+
MATCH (c:Conversation {sessionId: $sessionId})
|
|
2205
|
+
OPTIONAL MATCH (existingS:Person {telephone: $senderTelephone, accountId: $platformAccountId})
|
|
2206
|
+
WITH c, existingS IS NOT NULL AS senderReused
|
|
2207
|
+
MERGE (s:Person {telephone: $senderTelephone, accountId: $platformAccountId})
|
|
2208
|
+
ON CREATE SET
|
|
2209
|
+
s.givenName = $givenName,
|
|
2210
|
+
s.familyName = $familyName,
|
|
2211
|
+
s.source = 'whatsapp-live',
|
|
2212
|
+
s.scope = $scope,
|
|
2213
|
+
s.createdByAgent = 'whatsapp-live',
|
|
2214
|
+
s.createdBySource = 'whatsapp-live',
|
|
2215
|
+
s.createdAt = datetime()
|
|
2216
|
+
RETURN elementId(s) AS senderElementId, senderReused
|
|
2217
|
+
`;
|
|
2218
|
+
const params = {
|
|
2219
|
+
sessionId: input.sessionId,
|
|
2220
|
+
platformAccountId: input.platformAccountId,
|
|
2221
|
+
senderTelephone,
|
|
2222
|
+
givenName,
|
|
2223
|
+
familyName,
|
|
2224
|
+
scope
|
|
2225
|
+
};
|
|
2226
|
+
const result = await session.run(cypher, params);
|
|
2227
|
+
if (result.records.length === 0) {
|
|
2228
|
+
console.error(`${TAG9} skip reason=conversation-not-found accountId=${input.platformAccountId} cacheKey=${input.cacheKey} messageId=${messageId}`);
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
const rec = result.records[0];
|
|
2232
|
+
const senderElementId = rec.get("senderElementId");
|
|
2233
|
+
const senderReused = rec.get("senderReused") === true;
|
|
2234
|
+
console.error(
|
|
2235
|
+
`${TAG9} sender-resolved telephone=${senderTelephone} nodeKind=Person elementId=${senderElementId} reused=${senderReused}`
|
|
2236
|
+
);
|
|
2237
|
+
const diskRecord = {
|
|
2238
|
+
messageId,
|
|
2239
|
+
sessionId: input.sessionId,
|
|
2240
|
+
dateSent: dateSentIso,
|
|
2241
|
+
body: input.body,
|
|
2242
|
+
fromMe: input.fromMe,
|
|
2243
|
+
senderTelephone,
|
|
2244
|
+
senderName: input.pushName ?? null,
|
|
2245
|
+
remoteJid: canonicalRemoteJid,
|
|
2246
|
+
msgKeyId: input.msgKeyId,
|
|
2247
|
+
quotedId: input.quoted?.id ?? null,
|
|
2248
|
+
quotedSender: input.quoted?.sender ?? null,
|
|
2249
|
+
scope,
|
|
2250
|
+
...input.attachmentId ? { attachmentId: input.attachmentId } : {},
|
|
2251
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2252
|
+
};
|
|
2253
|
+
const altDedupeRemoteJids = canonicalRemoteJid !== input.remoteJid ? [input.remoteJid] : [];
|
|
2254
|
+
const { created } = appendMessage(input.platformAccountId, diskRecord, altDedupeRemoteJids);
|
|
2255
|
+
const ms = Date.now() - t0;
|
|
2256
|
+
if (!created) {
|
|
2257
|
+
console.error(`${TAG9} noop reason=existing-message messageId=${messageId}`);
|
|
2258
|
+
return { messageId, created: false, senderElementId };
|
|
2259
|
+
}
|
|
2260
|
+
console.error(
|
|
2261
|
+
`${TAG9} write messageId=${messageId} accountId=${input.platformAccountId} cacheKey=${input.cacheKey} fromMe=${input.fromMe} dateSent=${dateSentIso} bodyBytes=${Buffer.byteLength(input.body, "utf8")} attachment=${input.attachmentId ?? "none"} ms=${ms}`
|
|
2262
|
+
);
|
|
2263
|
+
return { messageId, created: true, senderElementId };
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
const ms = Date.now() - t0;
|
|
2266
|
+
const reason = sanitizeReason(err);
|
|
2267
|
+
console.error(
|
|
2268
|
+
`${TAG9} FAIL accountId=${input.platformAccountId} waname=${input.accountId} remoteJid=${input.remoteJid} msgKey=${input.msgKeyId} reason=${reason} ms=${ms}`
|
|
2269
|
+
);
|
|
2270
|
+
return null;
|
|
2271
|
+
} finally {
|
|
2272
|
+
release();
|
|
2273
|
+
if (sessionWriteLocks.get(input.cacheKey) === chained) {
|
|
2274
|
+
sessionWriteLocks.delete(input.cacheKey);
|
|
2275
|
+
}
|
|
2276
|
+
if (session) await session.close();
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function deriveScope(cacheKey) {
|
|
2280
|
+
const segments = cacheKey.split(":");
|
|
2281
|
+
return segments.length <= 2 ? "admin" : "public";
|
|
2282
|
+
}
|
|
2283
|
+
function splitName(pushName) {
|
|
2284
|
+
if (!pushName) return { givenName: null, familyName: null };
|
|
2285
|
+
const trimmed = pushName.trim();
|
|
2286
|
+
if (!trimmed) return { givenName: null, familyName: null };
|
|
2287
|
+
const firstSpace = trimmed.search(/\s/);
|
|
2288
|
+
if (firstSpace === -1) return { givenName: trimmed, familyName: null };
|
|
2289
|
+
return {
|
|
2290
|
+
givenName: trimmed.slice(0, firstSpace).trim(),
|
|
2291
|
+
familyName: trimmed.slice(firstSpace + 1).trim() || null
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
function sanitizeReason(err) {
|
|
2295
|
+
if (!(err instanceof Error)) return String(err).slice(0, 200);
|
|
2296
|
+
const msg = err.message.replace(/bolt:\/\/[^\s)]+/g, "bolt://<redacted>").replace(/\b(neo4j|admin|password)=[^\s)]+/gi, "$1=<redacted>").slice(0, 200);
|
|
2297
|
+
return `${err.name}:${msg}`;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// app/lib/whatsapp/ensure-conversation.ts
|
|
2301
|
+
var TAG10 = "[whatsapp-persist]";
|
|
2302
|
+
async function ensureWhatsAppConversation(input) {
|
|
2303
|
+
const t0 = Date.now();
|
|
2304
|
+
try {
|
|
2305
|
+
const result = await ensureConversation(
|
|
2306
|
+
input.platformAccountId,
|
|
2307
|
+
input.agentType,
|
|
2308
|
+
input.cacheKey
|
|
2309
|
+
);
|
|
2310
|
+
const ms = Date.now() - t0;
|
|
2311
|
+
if (!result.sessionId) {
|
|
2312
|
+
console.error(
|
|
2313
|
+
`${TAG10} conversation-merged FAIL cacheKey=${input.cacheKey} reason=null-sessionId ms=${ms}`
|
|
2314
|
+
);
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
console.error(
|
|
2318
|
+
`${TAG10} conversation-merged cacheKey=${input.cacheKey} agentType=${input.agentType} channel=whatsapp accountId=${input.platformAccountId} connectionAccountId=${input.connectionAccountId ?? input.platformAccountId} created=${result.created} ms=${ms}`
|
|
2319
|
+
);
|
|
2320
|
+
return { sessionId: result.sessionId, created: result.created };
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
const ms = Date.now() - t0;
|
|
2323
|
+
const reason = err instanceof Error ? `${err.name}:${err.message.slice(0, 200)}` : String(err).slice(0, 200);
|
|
2324
|
+
console.error(
|
|
2325
|
+
`${TAG10} conversation-merged FAIL cacheKey=${input.cacheKey} reason=${reason} ms=${ms}`
|
|
2326
|
+
);
|
|
2327
|
+
return null;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// ../lib/account-enumeration/src/index.ts
|
|
2332
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
|
|
2333
|
+
import { resolve as resolve2 } from "path";
|
|
2334
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2335
|
+
var cache2 = /* @__PURE__ */ new Map();
|
|
2336
|
+
function enumerateValidAccountIds(accountsDir) {
|
|
2337
|
+
const cached = cache2.get(accountsDir);
|
|
2338
|
+
if (cached !== void 0) return cached;
|
|
2339
|
+
let names;
|
|
2340
|
+
try {
|
|
2341
|
+
names = readdirSync3(accountsDir);
|
|
2342
|
+
} catch (err) {
|
|
2343
|
+
if (err.code === "ENOENT") {
|
|
2344
|
+
cache2.set(accountsDir, []);
|
|
2345
|
+
return [];
|
|
2346
|
+
}
|
|
2347
|
+
throw err;
|
|
2348
|
+
}
|
|
2349
|
+
const valid = [];
|
|
2350
|
+
for (const name of names) {
|
|
2351
|
+
if (!UUID_RE.test(name)) continue;
|
|
2352
|
+
const configPath = resolve2(accountsDir, name, "account.json");
|
|
2353
|
+
try {
|
|
2354
|
+
JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
2355
|
+
valid.push(name);
|
|
2356
|
+
} catch (err) {
|
|
2357
|
+
const code = err.code;
|
|
2358
|
+
if (code === "ENOENT") continue;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
cache2.set(accountsDir, valid);
|
|
2362
|
+
return valid;
|
|
2363
|
+
}
|
|
2364
|
+
function resolveHouseOrSoleAccountId(accountsDir) {
|
|
2365
|
+
const valid = enumerateValidAccountIds(accountsDir);
|
|
2366
|
+
const houses = [];
|
|
2367
|
+
for (const id of valid) {
|
|
2368
|
+
const configPath = resolve2(accountsDir, id, "account.json");
|
|
2369
|
+
try {
|
|
2370
|
+
const cfg = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
2371
|
+
if (cfg.role === "house") houses.push(id);
|
|
2372
|
+
} catch {
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (houses.length === 1) return houses[0];
|
|
2376
|
+
if (houses.length === 0 && valid.length === 1) return valid[0];
|
|
2377
|
+
throw new Error(
|
|
2378
|
+
`[account-registry] resolveHouseOrSoleAccountId: houses=${houses.length} total=${valid.length} under ${accountsDir} \u2014 expected exactly one role:"house" (or a single unlabelled account). Loud-fail rather than picking one silently.`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
function getAccountsDirFromEnv() {
|
|
2382
|
+
const root = process.env.MAXY_PLATFORM_ROOT;
|
|
2383
|
+
if (!root) {
|
|
2384
|
+
throw new Error(
|
|
2385
|
+
"[graph-write] MAXY_PLATFORM_ROOT not set \u2014 cannot enforce accountId gate. Set MAXY_PLATFORM_ROOT in the spawning process or pass `accountsDir` explicitly."
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
return resolve2(root, "..", "data/accounts");
|
|
2389
|
+
}
|
|
2390
|
+
function validateAccountIdEnv(envValue, diskIds) {
|
|
2391
|
+
if (!envValue) {
|
|
2392
|
+
return { ok: false, reason: "missing", envId: null, diskIds };
|
|
2393
|
+
}
|
|
2394
|
+
if (diskIds.length === 0) {
|
|
2395
|
+
return { ok: false, reason: "no-on-disk-account", envId: envValue, diskIds };
|
|
2396
|
+
}
|
|
2397
|
+
if (!diskIds.includes(envValue)) {
|
|
2398
|
+
return { ok: false, reason: "mismatch", envId: envValue, diskIds };
|
|
2399
|
+
}
|
|
2400
|
+
return { ok: true, envId: envValue, diskIds };
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// app/lib/whatsapp/platform-account-id.ts
|
|
2404
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2405
|
+
import { resolve as resolve3 } from "path";
|
|
2406
|
+
var cachedHouseId = null;
|
|
2407
|
+
var cachedHouseAccountsDir = null;
|
|
2408
|
+
function resolveHouseId(accountsDir) {
|
|
2409
|
+
if (cachedHouseId !== null && cachedHouseAccountsDir === accountsDir) return cachedHouseId;
|
|
2410
|
+
const resolved = resolveHouseOrSoleAccountId(accountsDir);
|
|
2411
|
+
cachedHouseId = resolved;
|
|
2412
|
+
cachedHouseAccountsDir = accountsDir;
|
|
2413
|
+
return cachedHouseId;
|
|
2414
|
+
}
|
|
2415
|
+
function readChannelRoutingOverride(accountsDir, houseId) {
|
|
2416
|
+
const configPath = resolve3(accountsDir, houseId, "account.json");
|
|
2417
|
+
let raw;
|
|
2418
|
+
try {
|
|
2419
|
+
raw = readFileSync6(configPath, "utf-8");
|
|
2420
|
+
} catch {
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
let cfg;
|
|
2424
|
+
try {
|
|
2425
|
+
cfg = JSON.parse(raw);
|
|
2426
|
+
} catch {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
const target = cfg.channelRoutingAccountId;
|
|
2430
|
+
if (typeof target !== "string" || !target) return null;
|
|
2431
|
+
const validIds = enumerateValidAccountIds(accountsDir);
|
|
2432
|
+
if (!validIds.includes(target)) {
|
|
2433
|
+
console.error(`[platform-account-id] channel-routing-invalid target=${target.slice(0, 8)} \u2014 falling back to house`);
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
return target;
|
|
2437
|
+
}
|
|
2438
|
+
function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
|
|
2439
|
+
const houseId = resolveHouseId(accountsDir);
|
|
2440
|
+
const routed = readChannelRoutingOverride(accountsDir, houseId);
|
|
2441
|
+
return routed ?? houseId;
|
|
2442
|
+
}
|
|
2443
|
+
function resolveHouseAccountId(accountsDir = ACCOUNTS_DIR) {
|
|
2444
|
+
return resolveHouseId(accountsDir);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// app/lib/whatsapp/account-id-shape.ts
|
|
2448
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2449
|
+
function isUuidAccountId(id) {
|
|
2450
|
+
return UUID_RE2.test(id);
|
|
2451
|
+
}
|
|
2452
|
+
function warnNonUuidAccount(caller, accountId) {
|
|
2453
|
+
if (isUuidAccountId(accountId)) return;
|
|
2454
|
+
console.error(
|
|
2455
|
+
`[whatsapp-account-scope] op=non-uuid-account caller=${caller} accountId=${accountId}`
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// app/lib/managed-service.ts
|
|
2460
|
+
import { join as join3 } from "path";
|
|
2461
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2462
|
+
function isHouseManagedService(accountsDir = ACCOUNTS_DIR) {
|
|
2463
|
+
let houseId;
|
|
2464
|
+
try {
|
|
2465
|
+
houseId = resolvePlatformAccountId(accountsDir);
|
|
2466
|
+
} catch {
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
try {
|
|
2470
|
+
const cfg = JSON.parse(
|
|
2471
|
+
readFileSync7(join3(accountsDir, houseId, "account.json"), "utf-8")
|
|
2472
|
+
);
|
|
2473
|
+
return cfg.managedService === true;
|
|
2474
|
+
} catch {
|
|
2475
|
+
return false;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function isCustomerTurnSuppressed(role, managedService) {
|
|
2479
|
+
return managedService === true && role !== "admin";
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// app/lib/whatsapp/inbound/media.ts
|
|
2483
|
+
import { randomUUID as randomUUID4, createHash } from "crypto";
|
|
2484
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
2485
|
+
import { join as join5 } from "path";
|
|
2486
|
+
import {
|
|
2487
|
+
downloadMediaMessage,
|
|
2488
|
+
downloadContentFromMessage,
|
|
2489
|
+
normalizeMessageContent as normalizeMessageContent2
|
|
2490
|
+
} from "@whiskeysockets/baileys";
|
|
2491
|
+
|
|
2492
|
+
// app/lib/attachments.ts
|
|
2493
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2494
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2495
|
+
import { realpathSync as realpathSync2, existsSync as existsSync4, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
|
|
2496
|
+
import { resolve as resolve5, extname } from "path";
|
|
2497
|
+
|
|
2498
|
+
// app/lib/data-path.ts
|
|
2499
|
+
import { realpathSync } from "fs";
|
|
2500
|
+
import { resolve as resolve4, normalize, sep, relative, basename, join as join4 } from "path";
|
|
2501
|
+
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
|
|
2502
|
+
var DATA_ROOT = resolve4(PLATFORM_ROOT2, "..", "data");
|
|
2503
|
+
var CLAUDE_UPLOADS_ROOT = process.env.CLAUDE_CONFIG_DIR ? resolve4(process.env.CLAUDE_CONFIG_DIR, "uploads") : null;
|
|
2504
|
+
var ACCOUNT_PARTITION_DIRS = /* @__PURE__ */ new Set(["accounts"]);
|
|
2505
|
+
var ACCOUNT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2506
|
+
function crossesForeignAccountPartition(relPath, accountId) {
|
|
2507
|
+
const segs = relPath.split("/").filter(Boolean);
|
|
2508
|
+
if (segs.length < 2) return false;
|
|
2509
|
+
if (!ACCOUNT_PARTITION_DIRS.has(segs[0])) return false;
|
|
2510
|
+
const owner = segs[1];
|
|
2511
|
+
if (!ACCOUNT_UUID_RE.test(owner)) return false;
|
|
2512
|
+
return owner !== accountId;
|
|
2513
|
+
}
|
|
2514
|
+
function isWithinOwnAccountPartition(relPath, accountId) {
|
|
2515
|
+
const segs = relPath.split("/").filter(Boolean);
|
|
2516
|
+
return segs.length >= 2 && segs[0] === "accounts" && segs[1] === accountId;
|
|
2517
|
+
}
|
|
2518
|
+
function resolveUnderRoot(raw, rootAbs, rootLabel) {
|
|
2519
|
+
const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
2520
|
+
const absolute = resolve4(rootAbs, cleaned);
|
|
2521
|
+
let rootReal;
|
|
2522
|
+
try {
|
|
2523
|
+
rootReal = realpathSync(rootAbs);
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
return {
|
|
2526
|
+
ok: false,
|
|
2527
|
+
status: 500,
|
|
2528
|
+
error: `${rootLabel} not accessible: ${err instanceof Error ? err.message : String(err)}`
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
let resolvedReal;
|
|
2532
|
+
try {
|
|
2533
|
+
resolvedReal = realpathSync(absolute);
|
|
2534
|
+
} catch (err) {
|
|
2535
|
+
const code = err.code;
|
|
2536
|
+
if (code === "ENOENT") {
|
|
2537
|
+
if (absolute !== rootReal && !absolute.startsWith(rootReal + sep)) {
|
|
2538
|
+
return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: absolute };
|
|
2539
|
+
}
|
|
2540
|
+
return { ok: false, status: 404, error: "Not found" };
|
|
2541
|
+
}
|
|
2542
|
+
return {
|
|
2543
|
+
ok: false,
|
|
2544
|
+
status: 500,
|
|
2545
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + sep)) {
|
|
2549
|
+
return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: resolvedReal };
|
|
2550
|
+
}
|
|
2551
|
+
const relPath = relative(rootReal, resolvedReal) || ".";
|
|
2552
|
+
return { ok: true, absolute: resolvedReal, dataRootReal: rootReal, relative: relPath };
|
|
2553
|
+
}
|
|
2554
|
+
function resolveDataPath(raw) {
|
|
2555
|
+
return resolveUnderRoot(raw, DATA_ROOT, "DATA_ROOT");
|
|
2556
|
+
}
|
|
2557
|
+
function resolveClaudeUploadsPath(raw) {
|
|
2558
|
+
if (!CLAUDE_UPLOADS_ROOT) {
|
|
2559
|
+
return { ok: false, status: 404, error: "Not found" };
|
|
2560
|
+
}
|
|
2561
|
+
return resolveUnderRoot(raw, CLAUDE_UPLOADS_ROOT, "CLAUDE_UPLOADS_ROOT");
|
|
2562
|
+
}
|
|
2563
|
+
function scratchpadTmpBase() {
|
|
2564
|
+
return process.env.MAXY_SCRATCHPAD_BASE ?? "/tmp";
|
|
2565
|
+
}
|
|
2566
|
+
function sessionScratchpadRoot(projectDir, sessionId) {
|
|
2567
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : 0;
|
|
2568
|
+
return join4(scratchpadTmpBase(), `claude-${uid}`, basename(projectDir), sessionId, "scratchpad");
|
|
2569
|
+
}
|
|
2570
|
+
function resolveScratchpadPath(raw, scratchpadRootAbs) {
|
|
2571
|
+
return resolveUnderRoot(raw, scratchpadRootAbs, "scratchpad");
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// app/lib/doc-to-text.ts
|
|
2575
|
+
import { unzipSync, strFromU8 } from "fflate";
|
|
2576
|
+
var DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
2577
|
+
var ODT = "application/vnd.oasis.opendocument.text";
|
|
2578
|
+
var XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
2579
|
+
var ODS = "application/vnd.oasis.opendocument.spreadsheet";
|
|
2580
|
+
var CONVERTIBLE_DOC_MIME = /* @__PURE__ */ new Set([
|
|
2581
|
+
DOCX,
|
|
2582
|
+
ODT,
|
|
2583
|
+
"text/rtf",
|
|
2584
|
+
"application/rtf",
|
|
2585
|
+
XLSX,
|
|
2586
|
+
ODS
|
|
2587
|
+
]);
|
|
2588
|
+
function bareMime(mimeType) {
|
|
2589
|
+
return mimeType.split(";")[0].trim().toLowerCase();
|
|
2590
|
+
}
|
|
2591
|
+
function unescapeXml(s) {
|
|
2592
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
2593
|
+
}
|
|
2594
|
+
function unzipEntry(buffer, entry) {
|
|
2595
|
+
const files = unzipSync(new Uint8Array(buffer));
|
|
2596
|
+
const bytes = files[entry];
|
|
2597
|
+
if (!bytes) throw new Error(`zip entry not found: ${entry}`);
|
|
2598
|
+
return strFromU8(bytes);
|
|
2599
|
+
}
|
|
2600
|
+
function docxToText(buffer) {
|
|
2601
|
+
const xml = unzipEntry(buffer, "word/document.xml");
|
|
2602
|
+
const paras = [];
|
|
2603
|
+
for (const p of xml.split("</w:p>")) {
|
|
2604
|
+
const withBreaks = p.replace(/<w:tab\b[^>]*\/?>/g, " ").replace(/<w:br\b[^>]*\/?>/g, "\n");
|
|
2605
|
+
let line = "";
|
|
2606
|
+
for (const m of withBreaks.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)) {
|
|
2607
|
+
line += unescapeXml(m[1]);
|
|
2608
|
+
}
|
|
2609
|
+
if (line.length > 0) paras.push(line);
|
|
2610
|
+
}
|
|
2611
|
+
return paras.join("\n");
|
|
2612
|
+
}
|
|
2613
|
+
function odtToText(buffer) {
|
|
2614
|
+
const xml = unzipEntry(buffer, "content.xml");
|
|
2615
|
+
const body = xml.replace(/^[\s\S]*?<office:text\b[^>]*>/, "").replace(/<\/office:text>[\s\S]*$/, "");
|
|
2616
|
+
const withBreaks = body.replace(/<text:line-break\b[^>]*\/?>/g, "\n").replace(/<text:tab\b[^>]*\/?>/g, " ").replace(/<\/text:(?:p|h)>/g, "\n");
|
|
2617
|
+
const stripped = withBreaks.replace(/<[^>]+>/g, "");
|
|
2618
|
+
return unescapeXml(stripped).replace(/\n{3,}/g, "\n\n").trim();
|
|
2619
|
+
}
|
|
2620
|
+
function sheetOrdinal(name) {
|
|
2621
|
+
const m = name.match(/sheet(\d+)\.xml$/);
|
|
2622
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
2623
|
+
}
|
|
2624
|
+
function xlsxToText(buffer) {
|
|
2625
|
+
const files = unzipSync(new Uint8Array(buffer));
|
|
2626
|
+
const shared = [];
|
|
2627
|
+
const sharedBytes = files["xl/sharedStrings.xml"];
|
|
2628
|
+
if (sharedBytes) {
|
|
2629
|
+
for (const si of strFromU8(sharedBytes).split("</si>")) {
|
|
2630
|
+
if (!si.includes("<si")) continue;
|
|
2631
|
+
const base = si.replace(/<rPh\b[\s\S]*?<\/rPh>/g, "");
|
|
2632
|
+
let s = "";
|
|
2633
|
+
for (const m of base.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)) s += m[1];
|
|
2634
|
+
shared.push(unescapeXml(s));
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const sheetKeys = Object.keys(files).filter((n) => /^xl\/worksheets\/sheet\d+\.xml$/.test(n)).sort((a, b) => sheetOrdinal(a) - sheetOrdinal(b));
|
|
2638
|
+
const sheets = [];
|
|
2639
|
+
for (const key of sheetKeys) {
|
|
2640
|
+
const xml = strFromU8(files[key]);
|
|
2641
|
+
const rows = [];
|
|
2642
|
+
for (const rowXml of xml.split("</row>")) {
|
|
2643
|
+
if (!rowXml.includes("<row")) continue;
|
|
2644
|
+
const cells = [];
|
|
2645
|
+
for (const c of rowXml.matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
|
|
2646
|
+
const attrs = c[1];
|
|
2647
|
+
const body = c[2];
|
|
2648
|
+
if (/\bt="inlineStr"/.test(attrs)) {
|
|
2649
|
+
let s = "";
|
|
2650
|
+
for (const m of body.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/g)) s += m[1];
|
|
2651
|
+
cells.push(unescapeXml(s));
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
const v = body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/);
|
|
2655
|
+
if (!v) {
|
|
2656
|
+
cells.push("");
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (/\bt="s"/.test(attrs)) {
|
|
2660
|
+
const idx = parseInt(v[1], 10);
|
|
2661
|
+
cells.push(Number.isInteger(idx) && idx >= 0 && idx < shared.length ? shared[idx] : "");
|
|
2662
|
+
} else {
|
|
2663
|
+
cells.push(unescapeXml(v[1]));
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
while (cells.length && cells[cells.length - 1] === "") cells.pop();
|
|
2667
|
+
if (cells.length) rows.push(cells.join(" "));
|
|
2668
|
+
}
|
|
2669
|
+
if (rows.length) sheets.push(rows.join("\n"));
|
|
2670
|
+
}
|
|
2671
|
+
return sheets.join("\n\n");
|
|
2672
|
+
}
|
|
2673
|
+
function odsToText(buffer) {
|
|
2674
|
+
const xml = unzipEntry(buffer, "content.xml");
|
|
2675
|
+
const tables = [];
|
|
2676
|
+
for (const tableXml of xml.split(/<table:table(?=[\s>])/).slice(1)) {
|
|
2677
|
+
const rows = [];
|
|
2678
|
+
for (const rowXml of tableXml.split(/<table:table-row\b/).slice(1)) {
|
|
2679
|
+
const cells = rowXml.split(/<table:(?:covered-)?table-cell\b/).slice(1).map((frag) => {
|
|
2680
|
+
const withBreaks = frag.replace(/<text:line-break\b[^>]*\/?>/g, "\n").replace(/<text:tab\b[^>]*\/?>/g, " ");
|
|
2681
|
+
let s = "";
|
|
2682
|
+
for (const m of withBreaks.matchAll(/<text:p\b[^>]*>([\s\S]*?)<\/text:p>/g)) {
|
|
2683
|
+
s += (s ? "\n" : "") + m[1].replace(/<[^>]+>/g, "");
|
|
2684
|
+
}
|
|
2685
|
+
return unescapeXml(s);
|
|
2686
|
+
});
|
|
2687
|
+
while (cells.length && cells[cells.length - 1] === "") cells.pop();
|
|
2688
|
+
if (cells.length) rows.push(cells.join(" "));
|
|
2689
|
+
}
|
|
2690
|
+
if (rows.length) tables.push(rows.join("\n"));
|
|
2691
|
+
}
|
|
2692
|
+
return tables.join("\n\n");
|
|
2693
|
+
}
|
|
2694
|
+
var RTF_DROP_DEST = /^\\(?:\*|fonttbl|colortbl|stylesheet|listtable|info|pict)\b/;
|
|
2695
|
+
function stripRtfDestinations(s) {
|
|
2696
|
+
let out = "";
|
|
2697
|
+
for (let i = 0; i < s.length; i++) {
|
|
2698
|
+
const ch = s[i];
|
|
2699
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
2700
|
+
out += ch + s[i + 1];
|
|
2701
|
+
i++;
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
if (ch === "{" && RTF_DROP_DEST.test(s.slice(i + 1))) {
|
|
2705
|
+
let depth = 1;
|
|
2706
|
+
let j = i + 1;
|
|
2707
|
+
for (; j < s.length && depth > 0; j++) {
|
|
2708
|
+
if (s[j] === "\\") {
|
|
2709
|
+
j++;
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
if (s[j] === "{") depth++;
|
|
2713
|
+
else if (s[j] === "}") depth--;
|
|
2714
|
+
}
|
|
2715
|
+
i = j - 1;
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
out += ch;
|
|
2719
|
+
}
|
|
2720
|
+
return out;
|
|
2721
|
+
}
|
|
2722
|
+
function rtfToText(rtf) {
|
|
2723
|
+
let s = stripRtfDestinations(rtf);
|
|
2724
|
+
s = s.replace(/\\par[d]?\b/g, "\n").replace(/\\line\b/g, "\n").replace(/\\tab\b/g, " ");
|
|
2725
|
+
s = s.replace(/\\'([0-9a-fA-F]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
|
|
2726
|
+
s = s.replace(/\\[a-zA-Z]+-?\d* ?/g, "");
|
|
2727
|
+
s = s.replace(/\\([{}\\])/g, "$1").replace(/[{}]/g, "");
|
|
2728
|
+
return s.replace(/[ \t]{2,}/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
2729
|
+
}
|
|
2730
|
+
function docToText(buffer, mimeType) {
|
|
2731
|
+
const mime = bareMime(mimeType);
|
|
2732
|
+
if (mime === DOCX) return docxToText(buffer);
|
|
2733
|
+
if (mime === ODT) return odtToText(buffer);
|
|
2734
|
+
if (mime === XLSX) return xlsxToText(buffer);
|
|
2735
|
+
if (mime === ODS) return odsToText(buffer);
|
|
2736
|
+
if (mime === "text/rtf" || mime === "application/rtf") return rtfToText(buffer.toString("latin1"));
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// app/lib/attachments.ts
|
|
2741
|
+
function uploadsDirFor(accountId, subPath, attachmentId) {
|
|
2742
|
+
return subPath ? resolve5(DATA_ROOT, "accounts", accountId, "uploads", subPath, attachmentId) : resolve5(DATA_ROOT, "accounts", accountId, "uploads", attachmentId);
|
|
2743
|
+
}
|
|
2744
|
+
var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
2745
|
+
"image/jpeg",
|
|
2746
|
+
"image/png",
|
|
2747
|
+
"image/gif",
|
|
2748
|
+
"image/webp",
|
|
2749
|
+
"application/pdf",
|
|
2750
|
+
"text/plain",
|
|
2751
|
+
"text/markdown",
|
|
2752
|
+
"text/csv",
|
|
2753
|
+
"text/html",
|
|
2754
|
+
"text/calendar",
|
|
2755
|
+
"application/zip",
|
|
2756
|
+
"application/x-zip-compressed",
|
|
2757
|
+
// Task 1347 — Word/OpenDocument/RTF text documents. The bytes are converted to
|
|
2758
|
+
// a readable-text sibling at ingestion (writeAttachment → doc-to-text.ts)
|
|
2759
|
+
// because the on-device Read tool cannot parse the binary containers. rtf is
|
|
2760
|
+
// reported by browsers as either text/rtf or application/rtf.
|
|
2761
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2762
|
+
"application/vnd.oasis.opendocument.text",
|
|
2763
|
+
"text/rtf",
|
|
2764
|
+
"application/rtf",
|
|
2765
|
+
// Task 1607 — spreadsheets + presentations operators send. These land
|
|
2766
|
+
// download-only in the reader; the Read tool cannot parse the binary
|
|
2767
|
+
// containers and agent-readable text extraction for them is task 1608.
|
|
2768
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2769
|
+
// xlsx
|
|
2770
|
+
"application/vnd.ms-excel",
|
|
2771
|
+
// xls
|
|
2772
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
2773
|
+
// ods
|
|
2774
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2775
|
+
// pptx
|
|
2776
|
+
"application/vnd.ms-powerpoint",
|
|
2777
|
+
// ppt
|
|
2778
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
2779
|
+
// odp
|
|
2780
|
+
// Task 1240 — voice recordings persist as playable clips in the admin bubble.
|
|
2781
|
+
// These match AUDIO_MIME_TYPES in app/lib/stt/audio-mime.ts exactly.
|
|
2782
|
+
"audio/ogg",
|
|
2783
|
+
"audio/opus",
|
|
2784
|
+
"audio/webm",
|
|
2785
|
+
"audio/mp4",
|
|
2786
|
+
"audio/x-m4a",
|
|
2787
|
+
"audio/mpeg",
|
|
2788
|
+
"audio/wav",
|
|
2789
|
+
"video/mp4"
|
|
2790
|
+
]);
|
|
2791
|
+
var MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024;
|
|
2792
|
+
var MAX_FILES_PER_MESSAGE = 5;
|
|
2793
|
+
var MAX_DATA_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
2794
|
+
var MAX_ZIP_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
2795
|
+
var EXTRACTED_TEXT_SUFFIX = ".extracted.txt";
|
|
2796
|
+
var ATTACHMENT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2797
|
+
function assertSupportedMime(mimeType) {
|
|
2798
|
+
const bare = mimeType.split(";")[0].trim().toLowerCase();
|
|
2799
|
+
if (!SUPPORTED_MIME_TYPES.has(bare)) {
|
|
2800
|
+
throw new Error(
|
|
2801
|
+
`Unsupported file type: "${mimeType}". Supported types: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
function isInlineSafeMime(mimeType) {
|
|
2806
|
+
const bare = mimeType.split(";")[0].trim().toLowerCase();
|
|
2807
|
+
if (bare === "image/svg+xml") return false;
|
|
2808
|
+
return bare.startsWith("image/") || bare === "application/pdf" || bare.startsWith("audio/") || bare === "video/mp4";
|
|
2809
|
+
}
|
|
2810
|
+
async function storeMediaBuffer(accountId, attachmentId, filename, mimeType, buffer) {
|
|
2811
|
+
if (buffer.byteLength > MAX_FILE_SIZE_BYTES) {
|
|
2812
|
+
throw new Error(
|
|
2813
|
+
`Media exceeds the 50 MB limit (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB).`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
return writeAttachment(accountId, "", filename, mimeType, buffer.byteLength, buffer, attachmentId);
|
|
2817
|
+
}
|
|
2818
|
+
function attachmentExists(accountId, attachmentId) {
|
|
2819
|
+
return existsSync4(resolve5(uploadsDirFor(accountId, "", attachmentId), `${attachmentId}.meta.json`));
|
|
2820
|
+
}
|
|
2821
|
+
function resolveStoredAttachment(accountId, attachmentId) {
|
|
2822
|
+
if (!ATTACHMENT_ID_RE.test(attachmentId)) return null;
|
|
2823
|
+
const dir = uploadsDirFor(accountId, "", attachmentId);
|
|
2824
|
+
let meta;
|
|
2825
|
+
try {
|
|
2826
|
+
meta = JSON.parse(readFileSync8(resolve5(dir, `${attachmentId}.meta.json`), "utf8"));
|
|
2827
|
+
} catch {
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
if (!meta || typeof meta !== "object") return null;
|
|
2831
|
+
const { filename, mimeType, sizeBytes } = meta;
|
|
2832
|
+
if (typeof filename !== "string" || typeof mimeType !== "string") return null;
|
|
2833
|
+
const resolvedSizeBytes = typeof sizeBytes === "number" ? sizeBytes : void 0;
|
|
2834
|
+
let entries;
|
|
2835
|
+
try {
|
|
2836
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2837
|
+
} catch {
|
|
2838
|
+
return null;
|
|
2839
|
+
}
|
|
2840
|
+
const dataFile = entries.find((e) => e.isFile() && !e.name.endsWith(".meta.json") && !e.name.endsWith(EXTRACTED_TEXT_SUFFIX));
|
|
2841
|
+
if (!dataFile) return null;
|
|
2842
|
+
const readableSibling = `${attachmentId}${EXTRACTED_TEXT_SUFFIX}`;
|
|
2843
|
+
const readableTextPath = entries.some((e) => e.isFile() && e.name === readableSibling) ? resolve5(dir, readableSibling) : void 0;
|
|
2844
|
+
return { attachmentId, filename, mimeType, sizeBytes: resolvedSizeBytes, path: resolve5(dir, dataFile.name), readableTextPath };
|
|
2845
|
+
}
|
|
2846
|
+
async function writeAttachment(accountId, subPath, filename, mimeType, sizeBytes, buffer, explicitId) {
|
|
2847
|
+
const attachmentId = explicitId ?? randomUUID3();
|
|
2848
|
+
const dir = uploadsDirFor(accountId, subPath, attachmentId);
|
|
2849
|
+
await mkdir(dir, { recursive: true });
|
|
2850
|
+
const ext = extname(filename) || "";
|
|
2851
|
+
const storagePath = resolve5(dir, `${attachmentId}${ext}`);
|
|
2852
|
+
const metaPath = resolve5(dir, `${attachmentId}.meta.json`);
|
|
2853
|
+
const meta = {
|
|
2854
|
+
attachmentId,
|
|
2855
|
+
scope: subPath || accountId,
|
|
2856
|
+
filename,
|
|
2857
|
+
mimeType,
|
|
2858
|
+
sizeBytes,
|
|
2859
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2860
|
+
};
|
|
2861
|
+
await writeFile(storagePath, buffer);
|
|
2862
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
2863
|
+
let readableTextPath;
|
|
2864
|
+
const bare = mimeType.split(";")[0].trim().toLowerCase();
|
|
2865
|
+
if (CONVERTIBLE_DOC_MIME.has(bare)) {
|
|
2866
|
+
let text = null;
|
|
2867
|
+
try {
|
|
2868
|
+
text = docToText(buffer, mimeType);
|
|
2869
|
+
} catch {
|
|
2870
|
+
text = null;
|
|
2871
|
+
}
|
|
2872
|
+
console.error(`[doc-to-text] op=convert mime=${bare} ok=${text !== null} chars=${text?.length ?? 0}`);
|
|
2873
|
+
if (text) {
|
|
2874
|
+
const siblingPath = resolve5(dir, `${attachmentId}${EXTRACTED_TEXT_SUFFIX}`);
|
|
2875
|
+
try {
|
|
2876
|
+
await writeFile(siblingPath, text);
|
|
2877
|
+
readableTextPath = siblingPath;
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
console.error(`[doc-to-text] op=sibling-write mime=${bare} ok=false reason=${String(err).slice(0, 80)}`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return { attachmentId, filename, storagePath, metaPath, mimeType, sizeBytes, readableTextPath };
|
|
2884
|
+
}
|
|
2885
|
+
async function storeAttachment(accountId, file) {
|
|
2886
|
+
assertSupportedMime(file.type);
|
|
2887
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
2888
|
+
throw new Error(
|
|
2889
|
+
`File "${file.name}" exceeds the 50 MB limit (${(file.size / 1024 / 1024).toFixed(1)} MB).`
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
2893
|
+
return writeAttachment(accountId, "", file.name, file.type, file.size, buffer);
|
|
2894
|
+
}
|
|
2895
|
+
async function storePublicAttachment(accountId, sessionId, file, buffer) {
|
|
2896
|
+
assertSupportedMime(file.type);
|
|
2897
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
2898
|
+
throw new Error(
|
|
2899
|
+
`File "${file.name}" exceeds the 50 MB limit (${(file.size / 1024 / 1024).toFixed(1)} MB).`
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
return writeAttachment(accountId, `public/${sessionId}`, file.name, file.type, file.size, buffer);
|
|
2903
|
+
}
|
|
2904
|
+
var MIME_BY_EXT = {
|
|
2905
|
+
".pdf": "application/pdf",
|
|
2906
|
+
".png": "image/png",
|
|
2907
|
+
".jpg": "image/jpeg",
|
|
2908
|
+
".jpeg": "image/jpeg",
|
|
2909
|
+
".gif": "image/gif",
|
|
2910
|
+
".webp": "image/webp",
|
|
2911
|
+
".svg": "image/svg+xml",
|
|
2912
|
+
".html": "text/html",
|
|
2913
|
+
".htm": "text/html",
|
|
2914
|
+
".css": "text/css",
|
|
2915
|
+
".js": "application/javascript",
|
|
2916
|
+
".json": "application/json",
|
|
2917
|
+
".csv": "text/csv",
|
|
2918
|
+
".txt": "text/plain",
|
|
2919
|
+
".md": "text/markdown",
|
|
2920
|
+
".xml": "text/xml",
|
|
2921
|
+
".zip": "application/zip",
|
|
2922
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2923
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2924
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
2925
|
+
".rtf": "text/rtf",
|
|
2926
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2927
|
+
// Task 1607 — legacy + OpenDocument office extensions, so a device-stored file
|
|
2928
|
+
// attached to a WhatsApp reply (detectMimeType → isMimeAdmitted, whatsapp-reader.ts)
|
|
2929
|
+
// resolves to the mime now admitted in SUPPORTED_MIME_TYPES rather than octet-stream.
|
|
2930
|
+
".xls": "application/vnd.ms-excel",
|
|
2931
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
2932
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
2933
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
2934
|
+
".ics": "text/calendar"
|
|
2935
|
+
};
|
|
2936
|
+
function detectMimeType(filePath) {
|
|
2937
|
+
const ext = extname(filePath).toLowerCase();
|
|
2938
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
2939
|
+
}
|
|
2940
|
+
function validateFilePathInAccount(filePath, accountDir) {
|
|
2941
|
+
const resolved = realpathSync2(filePath);
|
|
2942
|
+
const accountResolved = realpathSync2(accountDir);
|
|
2943
|
+
if (!resolved.startsWith(accountResolved + "/")) {
|
|
2944
|
+
throw new Error(`File path is outside the account directory`);
|
|
2945
|
+
}
|
|
2946
|
+
return resolved;
|
|
2947
|
+
}
|
|
2948
|
+
async function storeComponentArtefact(accountId, attachmentId, mimeType, content, filename) {
|
|
2949
|
+
const buffer = Buffer.from(content, "utf-8");
|
|
2950
|
+
if (buffer.byteLength > MAX_FILE_SIZE_BYTES) {
|
|
2951
|
+
throw new Error(
|
|
2952
|
+
`Component artefact exceeds the 50 MB limit (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB).`
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
const dir = uploadsDirFor(accountId, "", attachmentId);
|
|
2956
|
+
await mkdir(dir, { recursive: true });
|
|
2957
|
+
const ext = extname(filename) || (mimeType === "text/html" ? ".html" : ".md");
|
|
2958
|
+
const storagePath = resolve5(dir, `${attachmentId}${ext}`);
|
|
2959
|
+
const metaPath = resolve5(dir, `${attachmentId}.meta.json`);
|
|
2960
|
+
const meta = {
|
|
2961
|
+
attachmentId,
|
|
2962
|
+
scope: accountId,
|
|
2963
|
+
filename,
|
|
2964
|
+
mimeType,
|
|
2965
|
+
sizeBytes: buffer.byteLength,
|
|
2966
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2967
|
+
};
|
|
2968
|
+
await writeFile(storagePath, buffer);
|
|
2969
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
2970
|
+
return { attachmentId, filename, storagePath, metaPath, mimeType, sizeBytes: buffer.byteLength };
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// app/lib/whatsapp/inbound/media-download-guard.ts
|
|
2974
|
+
var TAG11 = "[media-guard]";
|
|
2975
|
+
var inFlight = 0;
|
|
2976
|
+
var containedCount = 0;
|
|
2977
|
+
function containedSinceBoot() {
|
|
2978
|
+
return containedCount;
|
|
2979
|
+
}
|
|
2980
|
+
function beginMediaDownload() {
|
|
2981
|
+
inFlight += 1;
|
|
2982
|
+
}
|
|
2983
|
+
function endMediaDownload() {
|
|
2984
|
+
if (inFlight > 0) inFlight -= 1;
|
|
2985
|
+
}
|
|
2986
|
+
function mediaDownloadsInFlight() {
|
|
2987
|
+
return inFlight;
|
|
2988
|
+
}
|
|
2989
|
+
function stackOf(err) {
|
|
2990
|
+
if (err instanceof Error) return err.stack ?? err.message;
|
|
2991
|
+
return String(err);
|
|
2992
|
+
}
|
|
2993
|
+
function causeCode(err) {
|
|
2994
|
+
const cause = err?.cause;
|
|
2995
|
+
const code = cause?.code;
|
|
2996
|
+
return typeof code === "string" ? code : "none";
|
|
2997
|
+
}
|
|
2998
|
+
function handleEscapedMediaError(err, channel) {
|
|
2999
|
+
const stack = stackOf(err);
|
|
3000
|
+
const undiciFrame = /node_modules[\\/]undici|node:internal[\\/]deps[\\/]undici/.test(stack);
|
|
3001
|
+
if (undiciFrame && inFlight > 0) {
|
|
3002
|
+
containedCount += 1;
|
|
3003
|
+
console.error(
|
|
3004
|
+
`${TAG11} op=contained channel=${channel} inFlight=${inFlight} err=${err instanceof Error ? err.message : String(err)} cause=${causeCode(err)}
|
|
3005
|
+
${stack}`
|
|
3006
|
+
);
|
|
3007
|
+
return true;
|
|
3008
|
+
}
|
|
3009
|
+
console.error(
|
|
3010
|
+
`${TAG11} op=escaped channel=${channel} inFlight=${inFlight} undiciFrame=${undiciFrame} err=${err instanceof Error ? err.message : String(err)} cause=${causeCode(err)} contained=false`
|
|
3011
|
+
);
|
|
3012
|
+
return false;
|
|
3013
|
+
}
|
|
3014
|
+
var installed = false;
|
|
3015
|
+
function guardInstalled() {
|
|
3016
|
+
return installed;
|
|
3017
|
+
}
|
|
3018
|
+
function installMediaDownloadGuard() {
|
|
3019
|
+
if (installed) return;
|
|
3020
|
+
installed = true;
|
|
3021
|
+
process.on("uncaughtException", (err) => {
|
|
3022
|
+
if (handleEscapedMediaError(err, "uncaughtException")) return;
|
|
3023
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
3024
|
+
`);
|
|
3025
|
+
process.exit(1);
|
|
3026
|
+
});
|
|
3027
|
+
process.on("unhandledRejection", (reason) => {
|
|
3028
|
+
if (handleEscapedMediaError(reason, "unhandledRejection")) return;
|
|
3029
|
+
process.stderr.write(
|
|
3030
|
+
`Unhandled rejection: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}
|
|
3031
|
+
`
|
|
3032
|
+
);
|
|
3033
|
+
process.exit(1);
|
|
3034
|
+
});
|
|
3035
|
+
console.error(`${TAG11} op=installed`);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// app/lib/whatsapp/inbound/media.ts
|
|
3039
|
+
var TAG12 = "[whatsapp:media]";
|
|
3040
|
+
var MEDIA_DIR = "/tmp/maxy-media";
|
|
3041
|
+
var DOWNLOAD_TIMEOUT_MS = 45e3;
|
|
3042
|
+
var DETACHED_GRACE_MS = 3e5;
|
|
3043
|
+
function deterministicAttachmentId(seed) {
|
|
3044
|
+
const h = createHash("sha256").update(seed).digest("hex");
|
|
3045
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
3046
|
+
}
|
|
3047
|
+
var CACHE_RETAIN_MS = 5e3;
|
|
3048
|
+
var inflightDownloads = /* @__PURE__ */ new Map();
|
|
3049
|
+
function downloadInboundMediaBufferCached(msg, sock, opts) {
|
|
3050
|
+
const key = msg.key.id ? `${msg.key.remoteJid ?? ""}:${msg.key.id}` : null;
|
|
3051
|
+
if (!key) return downloadInboundMediaBuffer(msg, sock, opts);
|
|
3052
|
+
const existing = inflightDownloads.get(key);
|
|
3053
|
+
if (existing) return existing;
|
|
3054
|
+
const p = downloadInboundMediaBuffer(msg, sock, opts);
|
|
3055
|
+
inflightDownloads.set(key, p);
|
|
3056
|
+
void p.then(
|
|
3057
|
+
(res) => {
|
|
3058
|
+
if (res === void 0) {
|
|
3059
|
+
inflightDownloads.delete(key);
|
|
3060
|
+
} else {
|
|
3061
|
+
const t = setTimeout(() => inflightDownloads.delete(key), CACHE_RETAIN_MS);
|
|
3062
|
+
t.unref?.();
|
|
3063
|
+
}
|
|
3064
|
+
},
|
|
3065
|
+
() => inflightDownloads.delete(key)
|
|
3066
|
+
// downloadInboundMediaBuffer never rejects, but be safe
|
|
3067
|
+
);
|
|
3068
|
+
return p;
|
|
3069
|
+
}
|
|
3070
|
+
function mimeToExt(mimetype) {
|
|
3071
|
+
const map = {
|
|
3072
|
+
"image/jpeg": "jpg",
|
|
3073
|
+
"image/png": "png",
|
|
3074
|
+
"image/webp": "webp",
|
|
3075
|
+
"image/gif": "gif",
|
|
3076
|
+
"audio/ogg; codecs=opus": "ogg",
|
|
3077
|
+
"audio/mpeg": "mp3",
|
|
3078
|
+
"audio/mp4": "m4a",
|
|
3079
|
+
"video/mp4": "mp4",
|
|
3080
|
+
"application/pdf": "pdf",
|
|
3081
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
3082
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx"
|
|
3083
|
+
};
|
|
3084
|
+
return map[mimetype] ?? mimetype.split("/").pop()?.replace(/[^a-z0-9]/gi, "") ?? "bin";
|
|
3085
|
+
}
|
|
3086
|
+
function getDownloadableContent(message) {
|
|
3087
|
+
const msg = message;
|
|
3088
|
+
if (msg.imageMessage) return { downloadable: msg.imageMessage, mediaType: "image" };
|
|
3089
|
+
if (msg.videoMessage) return { downloadable: msg.videoMessage, mediaType: "video" };
|
|
3090
|
+
if (msg.audioMessage) return { downloadable: msg.audioMessage, mediaType: "audio" };
|
|
3091
|
+
if (msg.documentMessage) return { downloadable: msg.documentMessage, mediaType: "document" };
|
|
3092
|
+
if (msg.stickerMessage) return { downloadable: msg.stickerMessage, mediaType: "sticker" };
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
async function streamToBuffer(stream) {
|
|
3096
|
+
const chunks = [];
|
|
3097
|
+
for await (const chunk of stream) {
|
|
3098
|
+
chunks.push(Buffer.from(chunk));
|
|
3099
|
+
}
|
|
3100
|
+
return Buffer.concat(chunks);
|
|
3101
|
+
}
|
|
3102
|
+
async function downloadInboundMediaBuffer(msg, sock, opts) {
|
|
3103
|
+
const rawMessage = msg.message;
|
|
3104
|
+
if (!rawMessage) return void 0;
|
|
3105
|
+
const content = normalizeMessageContent2(rawMessage);
|
|
3106
|
+
if (!content) return void 0;
|
|
3107
|
+
const inner = content;
|
|
3108
|
+
const mimetype = inner.imageMessage?.mimetype ?? inner.videoMessage?.mimetype ?? inner.audioMessage?.mimetype ?? inner.documentMessage?.mimetype ?? inner.stickerMessage?.mimetype ?? void 0;
|
|
3109
|
+
if (!inner.imageMessage && !inner.videoMessage && !inner.audioMessage && !inner.documentMessage && !inner.stickerMessage) {
|
|
3110
|
+
return void 0;
|
|
3111
|
+
}
|
|
3112
|
+
const maxBytes = opts?.maxBytes ?? 50 * 1024 * 1024;
|
|
3113
|
+
const timeoutMs = opts?.timeoutMs ?? DOWNLOAD_TIMEOUT_MS;
|
|
3114
|
+
beginMediaDownload();
|
|
3115
|
+
try {
|
|
3116
|
+
const settled = await withTimeout(
|
|
3117
|
+
`whatsapp media download (${mimetype ?? "unknown"})`,
|
|
3118
|
+
(async () => {
|
|
3119
|
+
let buffer = await downloadMediaMessage(
|
|
3120
|
+
msg,
|
|
3121
|
+
"buffer",
|
|
3122
|
+
{},
|
|
3123
|
+
{
|
|
3124
|
+
reuploadRequest: sock.updateMediaMessage,
|
|
3125
|
+
logger: sock.logger
|
|
3126
|
+
}
|
|
3127
|
+
);
|
|
3128
|
+
if (!buffer || buffer.length === 0) {
|
|
3129
|
+
console.error(`${TAG12} primary download returned empty, trying direct fallback`);
|
|
3130
|
+
const downloadable = getDownloadableContent(content);
|
|
3131
|
+
if (downloadable) {
|
|
3132
|
+
try {
|
|
3133
|
+
const stream = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
|
|
3134
|
+
buffer = await streamToBuffer(stream);
|
|
3135
|
+
} catch (fallbackErr) {
|
|
3136
|
+
console.error(`${TAG12} direct download fallback failed: ${String(fallbackErr)}`);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (!buffer || buffer.length === 0) {
|
|
3141
|
+
console.error(`${TAG12} download failed: empty buffer for ${mimetype ?? "unknown"}`);
|
|
3142
|
+
return void 0;
|
|
3143
|
+
}
|
|
3144
|
+
if (buffer.length > maxBytes) {
|
|
3145
|
+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
3146
|
+
const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
|
|
3147
|
+
console.error(`${TAG12} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
|
|
3148
|
+
return void 0;
|
|
3149
|
+
}
|
|
3150
|
+
return {
|
|
3151
|
+
buffer,
|
|
3152
|
+
mimetype: mimetype ?? "application/octet-stream",
|
|
3153
|
+
filename: inner.documentMessage?.fileName ?? void 0
|
|
3154
|
+
};
|
|
3155
|
+
})(),
|
|
3156
|
+
timeoutMs
|
|
3157
|
+
);
|
|
3158
|
+
endMediaDownload();
|
|
3159
|
+
return settled;
|
|
3160
|
+
} catch (err) {
|
|
3161
|
+
console.error(`${TAG12} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
|
|
3162
|
+
if (isTimeoutError(err)) {
|
|
3163
|
+
const t = setTimeout(endMediaDownload, DETACHED_GRACE_MS);
|
|
3164
|
+
t.unref?.();
|
|
3165
|
+
return void 0;
|
|
3166
|
+
}
|
|
3167
|
+
endMediaDownload();
|
|
3168
|
+
return void 0;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
async function downloadInboundMedia(msg, sock, opts) {
|
|
3172
|
+
const res = await downloadInboundMediaBufferCached(msg, sock, opts);
|
|
3173
|
+
if (!res) return void 0;
|
|
3174
|
+
await mkdir2(MEDIA_DIR, { recursive: true });
|
|
3175
|
+
const ext = mimeToExt(res.mimetype);
|
|
3176
|
+
const filePath = join5(MEDIA_DIR, `${randomUUID4()}.${ext}`);
|
|
3177
|
+
await writeFile2(filePath, res.buffer);
|
|
3178
|
+
const sizeKB = (res.buffer.length / 1024).toFixed(0);
|
|
3179
|
+
console.error(`${TAG12} media downloaded type=${res.mimetype} size=${sizeKB}KB path=${filePath}`);
|
|
3180
|
+
return { path: filePath, mimetype: res.mimetype, size: res.buffer.length };
|
|
3181
|
+
}
|
|
3182
|
+
async function storeInboundMediaServable(msg, sock, accountId, seed, opts) {
|
|
3183
|
+
const attachmentId = deterministicAttachmentId(seed);
|
|
3184
|
+
try {
|
|
3185
|
+
if (attachmentExists(accountId, attachmentId)) return attachmentId;
|
|
3186
|
+
const res = await downloadInboundMediaBufferCached(msg, sock, opts);
|
|
3187
|
+
if (!res) {
|
|
3188
|
+
console.error(`${TAG12} servable-store skip reason=no-buffer id=${attachmentId}`);
|
|
3189
|
+
return null;
|
|
3190
|
+
}
|
|
3191
|
+
const filename = res.filename ?? `${attachmentId}.${mimeToExt(res.mimetype)}`;
|
|
3192
|
+
await storeMediaBuffer(accountId, attachmentId, filename, res.mimetype, res.buffer);
|
|
3193
|
+
console.error(`${TAG12} servable-store ok id=${attachmentId} mime=${res.mimetype} bytes=${res.buffer.length}`);
|
|
3194
|
+
return attachmentId;
|
|
3195
|
+
} catch (err) {
|
|
3196
|
+
console.error(`${TAG12} servable-store FAIL id=${attachmentId} reason=${String(err).slice(0, 120)}`);
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// app/lib/whatsapp/outbound/document-limits.ts
|
|
3202
|
+
var WHATSAPP_DOCUMENT_MAX_BYTES = 100 * 1024 * 1024;
|
|
3203
|
+
|
|
3204
|
+
// app/lib/whatsapp/echo-store-max-bytes.ts
|
|
3205
|
+
function echoStoreMaxBytes(args) {
|
|
3206
|
+
return args.fromMe ? WHATSAPP_DOCUMENT_MAX_BYTES : (args.mediaMaxMb ?? 50) * 1024 * 1024;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
// app/lib/whatsapp/inbound/bounded-queue.ts
|
|
3210
|
+
function createBoundedQueue(limit, opts) {
|
|
3211
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
3212
|
+
throw new Error(`createBoundedQueue: limit must be a positive integer, got ${limit}`);
|
|
3213
|
+
}
|
|
3214
|
+
const maxPending = opts?.maxPending ?? Infinity;
|
|
3215
|
+
if (maxPending !== Infinity && (!Number.isInteger(maxPending) || maxPending < 1)) {
|
|
3216
|
+
throw new Error(`createBoundedQueue: maxPending must be a positive integer or omitted, got ${maxPending}`);
|
|
3217
|
+
}
|
|
3218
|
+
const queue = [];
|
|
3219
|
+
let running = 0;
|
|
3220
|
+
const pump = () => {
|
|
3221
|
+
while (running < limit && queue.length > 0) {
|
|
3222
|
+
const task = queue.shift();
|
|
3223
|
+
running++;
|
|
3224
|
+
void Promise.resolve().then(task).catch(() => {
|
|
3225
|
+
}).finally(() => {
|
|
3226
|
+
running--;
|
|
3227
|
+
pump();
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3231
|
+
return {
|
|
3232
|
+
schedule(task) {
|
|
3233
|
+
if (queue.length >= maxPending) return false;
|
|
3234
|
+
queue.push(task);
|
|
3235
|
+
pump();
|
|
3236
|
+
return true;
|
|
3237
|
+
},
|
|
3238
|
+
active: () => running,
|
|
3239
|
+
pending: () => queue.length
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// app/lib/whatsapp/inbound/debounce.ts
|
|
3244
|
+
var TAG13 = "[whatsapp:debounce]";
|
|
3245
|
+
var STT_TAG = "[whatsapp:stt-await]";
|
|
3246
|
+
function createInboundDebouncer(opts) {
|
|
3247
|
+
const { debounceMs, buildKey, onFlush, onError } = opts;
|
|
3248
|
+
const awaitTimeoutMs = opts.awaitTimeoutMs ?? 3e4;
|
|
3249
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3250
|
+
const pendingAwaits = /* @__PURE__ */ new Map();
|
|
3251
|
+
let destroyed = false;
|
|
3252
|
+
async function flushKey(key) {
|
|
3253
|
+
const awaiting = pendingAwaits.get(key);
|
|
3254
|
+
if (awaiting) {
|
|
3255
|
+
const start = Date.now();
|
|
3256
|
+
try {
|
|
3257
|
+
await Promise.race([
|
|
3258
|
+
awaiting,
|
|
3259
|
+
new Promise(
|
|
3260
|
+
(_, reject) => setTimeout(() => reject(new Error("stt-await-timeout")), awaitTimeoutMs)
|
|
3261
|
+
)
|
|
3262
|
+
]);
|
|
3263
|
+
console.error(`${STT_TAG} resolved key=${key} waited_ms=${Date.now() - start}`);
|
|
3264
|
+
} catch {
|
|
3265
|
+
console.error(`${STT_TAG} timeout key=${key} waited_ms=${Date.now() - start}`);
|
|
3266
|
+
}
|
|
3267
|
+
pendingAwaits.delete(key);
|
|
3268
|
+
}
|
|
3269
|
+
const batch = pending.get(key);
|
|
3270
|
+
if (!batch) return;
|
|
3271
|
+
clearTimeout(batch.timer);
|
|
3272
|
+
pending.delete(key);
|
|
3273
|
+
const batchSize = batch.entries.length;
|
|
3274
|
+
try {
|
|
3275
|
+
console.error(`${TAG13} debounce flush key=${key} batchSize=${batchSize}`);
|
|
3276
|
+
const result = onFlush(batch.entries);
|
|
3277
|
+
if (result && typeof result.catch === "function") {
|
|
3278
|
+
result.catch(onError);
|
|
3279
|
+
}
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
onError(err);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
async function flush() {
|
|
3285
|
+
const keys = Array.from(pending.keys());
|
|
3286
|
+
await Promise.all(keys.map((key) => flushKey(key)));
|
|
3287
|
+
}
|
|
3288
|
+
function enqueue(msg) {
|
|
3289
|
+
if (destroyed) return;
|
|
3290
|
+
if (debounceMs <= 0) {
|
|
3291
|
+
try {
|
|
3292
|
+
const result = onFlush([msg]);
|
|
3293
|
+
if (result && typeof result.catch === "function") {
|
|
3294
|
+
result.catch(onError);
|
|
3295
|
+
}
|
|
3296
|
+
} catch (err) {
|
|
3297
|
+
onError(err);
|
|
3298
|
+
}
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const key = buildKey(msg);
|
|
3302
|
+
if (!key) {
|
|
3303
|
+
try {
|
|
3304
|
+
const result = onFlush([msg]);
|
|
3305
|
+
if (result && typeof result.catch === "function") {
|
|
3306
|
+
result.catch(onError);
|
|
3307
|
+
}
|
|
3308
|
+
} catch (err) {
|
|
3309
|
+
onError(err);
|
|
3310
|
+
}
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
const existing = pending.get(key);
|
|
3314
|
+
if (existing) {
|
|
3315
|
+
existing.entries.push(msg);
|
|
3316
|
+
clearTimeout(existing.timer);
|
|
3317
|
+
existing.timer = setTimeout(() => {
|
|
3318
|
+
flushKey(key).catch(onError);
|
|
3319
|
+
}, debounceMs);
|
|
3320
|
+
} else {
|
|
3321
|
+
const timer = setTimeout(() => {
|
|
3322
|
+
flushKey(key).catch(onError);
|
|
3323
|
+
}, debounceMs);
|
|
3324
|
+
pending.set(key, { entries: [msg], timer });
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
function registerPending(key, promise) {
|
|
3328
|
+
if (destroyed) return;
|
|
3329
|
+
const existing = pendingAwaits.get(key);
|
|
3330
|
+
if (existing) {
|
|
3331
|
+
pendingAwaits.set(key, Promise.all([existing, promise]).then(() => {
|
|
3332
|
+
}));
|
|
3333
|
+
} else {
|
|
3334
|
+
pendingAwaits.set(key, promise);
|
|
3335
|
+
}
|
|
3336
|
+
console.error(`${STT_TAG} pending key=${key}`);
|
|
3337
|
+
}
|
|
3338
|
+
async function destroy() {
|
|
3339
|
+
if (destroyed) return;
|
|
3340
|
+
destroyed = true;
|
|
3341
|
+
await flush();
|
|
3342
|
+
}
|
|
3343
|
+
return { enqueue, flush, destroy, registerPending };
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// app/lib/whatsapp/inbound/combine-batch.ts
|
|
3347
|
+
function combineInboundBatch(entries) {
|
|
3348
|
+
const last = entries[entries.length - 1];
|
|
3349
|
+
const combinedText = entries.map((e) => e.text).filter(Boolean).join("\n");
|
|
3350
|
+
const media = entries.flatMap((e) => e.media);
|
|
3351
|
+
return { ...last, text: combinedText, media };
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3354
|
+
// app/lib/whatsapp/opening-hours.ts
|
|
3355
|
+
var TAG14 = "[whatsapp:hours]";
|
|
3356
|
+
async function isBusinessOpen(accountId) {
|
|
3357
|
+
try {
|
|
3358
|
+
const timezone = await resolveTimezone(accountId);
|
|
3359
|
+
const now = /* @__PURE__ */ new Date();
|
|
3360
|
+
const dayOfWeek = getDayOfWeek(now, timezone);
|
|
3361
|
+
const previousDayOfWeek = getPreviousDayOfWeek(now, timezone);
|
|
3362
|
+
const currentTime = getCurrentTime(now, timezone);
|
|
3363
|
+
const session = getSession();
|
|
3364
|
+
try {
|
|
3365
|
+
const result = await session.run(
|
|
3366
|
+
`MATCH (b:LocalBusiness {accountId: $accountId})
|
|
3367
|
+
OPTIONAL MATCH (b)-[:HAS_HOURS]->(hours:OpeningHoursSpecification)
|
|
3368
|
+
WHERE hours.dayOfWeek IN [$dayOfWeek, $previousDayOfWeek]
|
|
3369
|
+
RETURN hours.dayOfWeek AS dayOfWeek, hours.opens AS opens, hours.closes AS closes`,
|
|
3370
|
+
{ accountId, dayOfWeek, previousDayOfWeek }
|
|
3371
|
+
);
|
|
3372
|
+
if (result.records.length === 0) {
|
|
3373
|
+
console.error(`${TAG14} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
3374
|
+
return { open: true, reason: "no opening hours configured" };
|
|
3375
|
+
}
|
|
3376
|
+
const specs = result.records.filter((r) => r.get("opens") != null && r.get("closes") != null).map((r) => ({
|
|
3377
|
+
day: String(r.get("dayOfWeek")).trim(),
|
|
3378
|
+
opens: String(r.get("opens")).trim(),
|
|
3379
|
+
closes: String(r.get("closes")).trim()
|
|
3380
|
+
}));
|
|
3381
|
+
if (specs.length === 0) {
|
|
3382
|
+
console.error(`${TAG14} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
3383
|
+
return { open: true, reason: "no opening hours configured" };
|
|
3384
|
+
}
|
|
3385
|
+
for (const spec of specs) {
|
|
3386
|
+
if (!spec.opens || !spec.closes || spec.opens === spec.closes) continue;
|
|
3387
|
+
const isYesterday = spec.day === previousDayOfWeek;
|
|
3388
|
+
if (isYesterday) {
|
|
3389
|
+
if (spec.opens > spec.closes && currentTime < spec.closes) {
|
|
3390
|
+
const hoursStr = `${spec.opens}-${spec.closes} (${spec.day})`;
|
|
3391
|
+
console.error(
|
|
3392
|
+
`${TAG14} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
3393
|
+
);
|
|
3394
|
+
return {
|
|
3395
|
+
open: true,
|
|
3396
|
+
reason: "within business hours",
|
|
3397
|
+
dayOfWeek,
|
|
3398
|
+
currentTime,
|
|
3399
|
+
matchedHours: hoursStr
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
} else if (isTimeInRange(currentTime, spec.opens, spec.closes)) {
|
|
3403
|
+
const hoursStr = `${spec.opens}-${spec.closes}`;
|
|
3404
|
+
console.error(
|
|
3405
|
+
`${TAG14} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
3406
|
+
);
|
|
3407
|
+
return {
|
|
3408
|
+
open: true,
|
|
3409
|
+
reason: "within business hours",
|
|
3410
|
+
dayOfWeek,
|
|
3411
|
+
currentTime,
|
|
3412
|
+
matchedHours: hoursStr
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
const todaySpecs = specs.filter((s) => s.day === dayOfWeek);
|
|
3417
|
+
const allHours = todaySpecs.map((s) => `${s.opens}-${s.closes}`).join(", ") || "none today";
|
|
3418
|
+
console.error(
|
|
3419
|
+
`${TAG14} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
|
|
3420
|
+
);
|
|
3421
|
+
return {
|
|
3422
|
+
open: false,
|
|
3423
|
+
reason: "outside business hours",
|
|
3424
|
+
dayOfWeek,
|
|
3425
|
+
currentTime,
|
|
3426
|
+
matchedHours: allHours
|
|
3427
|
+
};
|
|
3428
|
+
} finally {
|
|
3429
|
+
await session.close();
|
|
3430
|
+
}
|
|
3431
|
+
} catch (err) {
|
|
3432
|
+
console.error(
|
|
3433
|
+
`${TAG14} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
|
|
3434
|
+
);
|
|
3435
|
+
return { open: true, reason: "hours check failed (treating as open)" };
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
async function resolveTimezone(accountId) {
|
|
3439
|
+
try {
|
|
3440
|
+
const tz = await getUserTimezone(accountId);
|
|
3441
|
+
if (tz) {
|
|
3442
|
+
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
3443
|
+
return tz;
|
|
3444
|
+
}
|
|
3445
|
+
} catch {
|
|
3446
|
+
}
|
|
3447
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3448
|
+
}
|
|
3449
|
+
function getDayOfWeek(date, timezone) {
|
|
3450
|
+
return new Intl.DateTimeFormat("en-US", { weekday: "long", timeZone: timezone }).format(date);
|
|
3451
|
+
}
|
|
3452
|
+
function getPreviousDayOfWeek(date, timezone) {
|
|
3453
|
+
const yesterday = new Date(date.getTime() - 24 * 60 * 60 * 1e3);
|
|
3454
|
+
return getDayOfWeek(yesterday, timezone);
|
|
3455
|
+
}
|
|
3456
|
+
function getCurrentTime(date, timezone) {
|
|
3457
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
3458
|
+
hour: "2-digit",
|
|
3459
|
+
minute: "2-digit",
|
|
3460
|
+
hour12: false,
|
|
3461
|
+
timeZone: timezone
|
|
3462
|
+
}).formatToParts(date);
|
|
3463
|
+
const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
|
|
3464
|
+
const minute = parts.find((p) => p.type === "minute")?.value ?? "00";
|
|
3465
|
+
return `${hour}:${minute}`;
|
|
3466
|
+
}
|
|
3467
|
+
function isTimeInRange(time, opens, closes) {
|
|
3468
|
+
if (opens <= closes) {
|
|
3469
|
+
return time >= opens && time < closes;
|
|
3470
|
+
}
|
|
3471
|
+
return time >= opens || time < closes;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// app/lib/stt/index.ts
|
|
3475
|
+
import { execFile } from "child_process";
|
|
3476
|
+
import { unlink, stat } from "fs/promises";
|
|
3477
|
+
import { promisify } from "util";
|
|
3478
|
+
var execFileAsync = promisify(execFile);
|
|
3479
|
+
var TAG15 = "[stt]";
|
|
3480
|
+
var WHISPER_BINARY = process.env.WHISPER_BINARY ?? "/opt/whisper.cpp/build/bin/whisper-cli";
|
|
3481
|
+
var WHISPER_MODEL = process.env.WHISPER_MODEL ?? "/opt/whisper.cpp/models/ggml-base.bin";
|
|
3482
|
+
var WHISPER_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
3483
|
+
var SPECIAL_TOKEN_RE = /\[_[A-Z]+(?:_\d+)?_?\]/g;
|
|
3484
|
+
function stripSpecialTokens(input) {
|
|
3485
|
+
const matches = input.match(SPECIAL_TOKEN_RE);
|
|
3486
|
+
const count = matches ? matches.length : 0;
|
|
3487
|
+
if (!count) return { text: input, count: 0 };
|
|
3488
|
+
const text = input.replace(SPECIAL_TOKEN_RE, "").replace(/[ \t]{2,}/g, " ").trim();
|
|
3489
|
+
return { text, count };
|
|
3490
|
+
}
|
|
3491
|
+
async function transcribe(audioPath, mimetype) {
|
|
3492
|
+
const startMs = Date.now();
|
|
3493
|
+
let audioBytes = 0;
|
|
3494
|
+
try {
|
|
3495
|
+
const s = await stat(audioPath);
|
|
3496
|
+
audioBytes = s.size;
|
|
3497
|
+
} catch {
|
|
3498
|
+
console.error(`${TAG15} failed: file not readable path=${audioPath}`);
|
|
3499
|
+
return void 0;
|
|
3500
|
+
}
|
|
3501
|
+
console.error(
|
|
3502
|
+
`${TAG15} start provider=whisper-local audio_bytes=${audioBytes} mimetype=${mimetype} path=${audioPath}`
|
|
3503
|
+
);
|
|
3504
|
+
const wavPath = audioPath.replace(/\.[^.]+$/, "") + ".wav";
|
|
3505
|
+
try {
|
|
3506
|
+
await execFileAsync("ffmpeg", [
|
|
3507
|
+
"-i",
|
|
3508
|
+
audioPath,
|
|
3509
|
+
"-ar",
|
|
3510
|
+
"16000",
|
|
3511
|
+
"-ac",
|
|
3512
|
+
"1",
|
|
3513
|
+
"-c:a",
|
|
3514
|
+
"pcm_s16le",
|
|
3515
|
+
"-y",
|
|
3516
|
+
// overwrite without prompting
|
|
3517
|
+
wavPath
|
|
3518
|
+
], { timeout: 3e4 });
|
|
3519
|
+
} catch (err) {
|
|
3520
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3521
|
+
console.error(`${TAG15} failed: ffmpeg conversion error=${reason}`);
|
|
3522
|
+
return void 0;
|
|
3523
|
+
}
|
|
3524
|
+
try {
|
|
3525
|
+
const { stdout, stderr } = await execFileAsync(WHISPER_BINARY, [
|
|
3526
|
+
"-m",
|
|
3527
|
+
WHISPER_MODEL,
|
|
3528
|
+
"-f",
|
|
3529
|
+
wavPath,
|
|
3530
|
+
"--no-timestamps",
|
|
3531
|
+
"-l",
|
|
3532
|
+
"auto"
|
|
3533
|
+
], { timeout: WHISPER_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
|
|
3534
|
+
const durationMs = Date.now() - startMs;
|
|
3535
|
+
const { text, count: stripped } = stripSpecialTokens(stdout.trim());
|
|
3536
|
+
if (!text) {
|
|
3537
|
+
console.error(`${TAG15} failed: whisper returned empty output duration_ms=${durationMs}`);
|
|
3538
|
+
return void 0;
|
|
3539
|
+
}
|
|
3540
|
+
const langMatch = stderr.match(/auto-detected language:\s*(\w+)/);
|
|
3541
|
+
const language = langMatch?.[1] ?? "unknown";
|
|
3542
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
3543
|
+
console.error(
|
|
3544
|
+
`${TAG15} done provider=whisper-local duration_ms=${durationMs} words=${words} stripped=${stripped} lang=${language}`
|
|
3545
|
+
);
|
|
3546
|
+
return { text, language, durationMs };
|
|
3547
|
+
} catch (err) {
|
|
3548
|
+
const durationMs = Date.now() - startMs;
|
|
3549
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3550
|
+
console.error(`${TAG15} failed provider=whisper-local duration_ms=${durationMs} error=${reason}`);
|
|
3551
|
+
return void 0;
|
|
3552
|
+
} finally {
|
|
3553
|
+
unlink(wavPath).catch(() => {
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
// app/lib/whatsapp/reply-error.ts
|
|
3559
|
+
var WaReplyError = class extends Error {
|
|
3560
|
+
terminal;
|
|
3561
|
+
constructor(message, terminal) {
|
|
3562
|
+
super(message);
|
|
3563
|
+
this.name = "WaReplyError";
|
|
3564
|
+
this.terminal = terminal;
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
// app/lib/whatsapp/manager.ts
|
|
3569
|
+
var TAG16 = "[whatsapp:manager]";
|
|
3570
|
+
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
3571
|
+
var MESSAGE_STORE_MAX = 500;
|
|
3572
|
+
var APPEND_MEDIA_CONCURRENCY = 3;
|
|
3573
|
+
var APPEND_MEDIA_MAX_PENDING = 24;
|
|
3574
|
+
var appendMediaQueue = createBoundedQueue(APPEND_MEDIA_CONCURRENCY, {
|
|
3575
|
+
maxPending: APPEND_MEDIA_MAX_PENDING
|
|
3576
|
+
});
|
|
3577
|
+
var connections = /* @__PURE__ */ new Map();
|
|
3578
|
+
var configDir = null;
|
|
3579
|
+
var whatsAppConfig = {};
|
|
3580
|
+
var onInboundMessage = null;
|
|
3581
|
+
var onNotify = null;
|
|
3582
|
+
function buildNotifyPending(args) {
|
|
3583
|
+
const { body, truncated } = formatNotifyBody(args.text, args.mediaType);
|
|
3584
|
+
const trimmed = args.pushName?.trim() ?? "";
|
|
3585
|
+
return {
|
|
3586
|
+
waMessageId: args.msgKeyId,
|
|
3587
|
+
accountId: args.persistAccountId,
|
|
3588
|
+
ledgerAccount: args.ledgerAccount,
|
|
3589
|
+
senderType: args.senderType,
|
|
3590
|
+
senderDisplay: trimmed.length > 0 ? trimmed : formatSenderPhone(args.senderPhone),
|
|
3591
|
+
body,
|
|
3592
|
+
truncated,
|
|
3593
|
+
ts: args.ts
|
|
3594
|
+
};
|
|
3595
|
+
}
|
|
3596
|
+
var initialized = false;
|
|
3597
|
+
function dispatchInboundPayload(conn, payload) {
|
|
3598
|
+
if (!onInboundMessage) return;
|
|
3599
|
+
decideDispatch(conn, payload, { config: whatsAppConfig, emit: onInboundMessage, log: (l) => console.error(l) });
|
|
3600
|
+
}
|
|
3601
|
+
function isObserveAccount(accountId) {
|
|
3602
|
+
return !shouldDispatchInbound(whatsAppConfig, accountId);
|
|
3603
|
+
}
|
|
3604
|
+
function getWhatsAppConfig() {
|
|
3605
|
+
return whatsAppConfig;
|
|
3606
|
+
}
|
|
3607
|
+
var messageStore = /* @__PURE__ */ new Map();
|
|
3608
|
+
function messageStoreKey(accountId, remoteJid) {
|
|
3609
|
+
return `${accountId}:${remoteJid}`;
|
|
3610
|
+
}
|
|
3611
|
+
function storeMessage(storeKey, entry) {
|
|
3612
|
+
let entries = messageStore.get(storeKey);
|
|
3613
|
+
if (!entries) {
|
|
3614
|
+
entries = [];
|
|
3615
|
+
messageStore.set(storeKey, entries);
|
|
3616
|
+
}
|
|
3617
|
+
entries.push(entry);
|
|
3618
|
+
if (entries.length > MESSAGE_STORE_MAX) {
|
|
3619
|
+
const trimmed = entries.length - MESSAGE_STORE_MAX;
|
|
3620
|
+
entries.splice(0, trimmed);
|
|
3621
|
+
console.error(`${TAG16} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function deriveCacheKey(input) {
|
|
3625
|
+
if (input.isOwnerMirror || input.agentType === "admin") {
|
|
3626
|
+
return `whatsapp:${input.accountId}`;
|
|
3627
|
+
}
|
|
3628
|
+
if (input.isGroup) {
|
|
3629
|
+
if (!input.groupJid) {
|
|
3630
|
+
throw new Error(
|
|
3631
|
+
`deriveCacheKey: isGroup=true requires groupJid (accountId=${input.accountId}, senderPhone=${input.senderPhone})`
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
return `whatsapp:${input.accountId}:group:${input.groupJid}`;
|
|
3635
|
+
}
|
|
3636
|
+
return `whatsapp:${input.accountId}:${input.senderPhone}`;
|
|
3637
|
+
}
|
|
3638
|
+
function buildSelfChatDispatch(input) {
|
|
3639
|
+
const senderPhone = input.selfPhone ?? "owner";
|
|
3640
|
+
return {
|
|
3641
|
+
accountId: input.accountId,
|
|
3642
|
+
agentType: "admin",
|
|
3643
|
+
senderPhone,
|
|
3644
|
+
text: input.text,
|
|
3645
|
+
isGroup: false,
|
|
3646
|
+
reply: input.reply,
|
|
3647
|
+
composing: input.composing,
|
|
3648
|
+
cacheKey: deriveCacheKey({
|
|
3649
|
+
agentType: "admin",
|
|
3650
|
+
accountId: input.accountId,
|
|
3651
|
+
senderPhone,
|
|
3652
|
+
isGroup: false
|
|
3653
|
+
}),
|
|
3654
|
+
media: [],
|
|
3655
|
+
// Self-chat is the owner: the session scopes to the house account (Task 1383),
|
|
3656
|
+
// named by its platform UUID (Task 1850).
|
|
3657
|
+
effectiveAccountId: input.platformAccountId,
|
|
3658
|
+
// Task 1575 — self-chat is unambiguously the owner, so attribute the spawn
|
|
3659
|
+
// to the owner userId even when the owner phone is unbound in users.json.
|
|
3660
|
+
ownerSelfPhone: true
|
|
3661
|
+
};
|
|
3662
|
+
}
|
|
3663
|
+
async function init(opts) {
|
|
3664
|
+
if (initialized) {
|
|
3665
|
+
console.error(`${TAG16} already initialized`);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
configDir = opts.configDir;
|
|
3669
|
+
onInboundMessage = opts.onMessage;
|
|
3670
|
+
onNotify = opts.onNotify ?? null;
|
|
3671
|
+
loadConfig(opts.accountConfig);
|
|
3672
|
+
const accountIds = listCredentialAccountIds(configDir);
|
|
3673
|
+
if (accountIds.length === 0) {
|
|
3674
|
+
console.error(`${TAG16} init: no stored WhatsApp credentials found`);
|
|
3675
|
+
initialized = true;
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
console.error(`${TAG16} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
|
|
3679
|
+
initialized = true;
|
|
3680
|
+
for (const accountId of accountIds) {
|
|
3681
|
+
const accountCfg = whatsAppConfig.accounts?.[accountId];
|
|
3682
|
+
if (accountCfg?.enabled === false) {
|
|
3683
|
+
console.error(`${TAG16} skipping disabled account=${accountId}`);
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
startConnection(accountId).catch((err) => {
|
|
3687
|
+
console.error(`${TAG16} failed to auto-start account=${accountId}: ${formatError(err)}`);
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
async function startConnection(accountId) {
|
|
3692
|
+
if (!configDir) throw new Error("WhatsApp manager not initialized");
|
|
3693
|
+
const authDir = resolveAuthDir(configDir, accountId);
|
|
3694
|
+
const hasAuth = await authExists(authDir);
|
|
3695
|
+
if (!hasAuth) {
|
|
3696
|
+
console.error(`${TAG16} no credentials for account=${accountId}`);
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
let platformAccountId;
|
|
3700
|
+
let houseAccountId;
|
|
3701
|
+
try {
|
|
3702
|
+
platformAccountId = resolvePlatformAccountId();
|
|
3703
|
+
houseAccountId = resolveHouseAccountId();
|
|
3704
|
+
console.error(
|
|
3705
|
+
`[whatsapp-persist] resolved-account-id waname=${accountId} uuid=${platformAccountId} house=${houseAccountId}`
|
|
3706
|
+
);
|
|
3707
|
+
} catch (err) {
|
|
3708
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3709
|
+
console.error(
|
|
3710
|
+
`[whatsapp-persist] resolved-account-id FAIL waname=${accountId} reason=${reason}`
|
|
3711
|
+
);
|
|
3712
|
+
throw err;
|
|
3713
|
+
}
|
|
3714
|
+
await stopConnection(accountId);
|
|
3715
|
+
const conn = {
|
|
3716
|
+
accountId,
|
|
3717
|
+
platformAccountId,
|
|
3718
|
+
houseAccountId,
|
|
3719
|
+
accountName: whatsAppConfig.accounts?.[accountId]?.name,
|
|
3720
|
+
authDir,
|
|
3721
|
+
sock: null,
|
|
3722
|
+
connected: false,
|
|
3723
|
+
selfPhone: null,
|
|
3724
|
+
selfJid: null,
|
|
3725
|
+
selfLid: null,
|
|
3726
|
+
reconnectAttempts: 0,
|
|
3727
|
+
abortController: new AbortController(),
|
|
3728
|
+
debouncer: null,
|
|
3729
|
+
lidMapping: null
|
|
3730
|
+
};
|
|
3731
|
+
connections.set(accountId, conn);
|
|
3732
|
+
await connectWithReconnect(conn);
|
|
3733
|
+
}
|
|
3734
|
+
async function stopConnection(accountId) {
|
|
3735
|
+
const conn = connections.get(accountId);
|
|
3736
|
+
if (!conn) return;
|
|
3737
|
+
conn.abortController.abort();
|
|
3738
|
+
if (conn.debouncer) {
|
|
3739
|
+
await conn.debouncer.destroy();
|
|
3740
|
+
conn.debouncer = null;
|
|
3741
|
+
}
|
|
3742
|
+
if (conn.sock) {
|
|
3743
|
+
try {
|
|
3744
|
+
conn.sock.ev.removeAllListeners("messages.upsert");
|
|
3745
|
+
conn.sock.ev.removeAllListeners("messages.reaction");
|
|
3746
|
+
conn.sock.ev.removeAllListeners("connection.update");
|
|
3747
|
+
conn.sock.ev.removeAllListeners("creds.update");
|
|
3748
|
+
conn.sock.ws?.close?.();
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
console.warn(`${TAG16} socket cleanup error during stop account=${accountId}: ${String(err)}`);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
connections.delete(accountId);
|
|
3754
|
+
console.error(`${TAG16} stopped account=${accountId}`);
|
|
3755
|
+
}
|
|
3756
|
+
function getStatus() {
|
|
3757
|
+
return Array.from(connections.values()).map((conn) => ({
|
|
3758
|
+
accountId: conn.accountId,
|
|
3759
|
+
accountName: conn.accountName,
|
|
3760
|
+
connected: conn.connected,
|
|
3761
|
+
selfPhone: conn.selfPhone ?? void 0,
|
|
3762
|
+
selfJid: conn.selfJid ?? void 0,
|
|
3763
|
+
reconnectAttempts: conn.reconnectAttempts,
|
|
3764
|
+
lastConnectedAt: conn.lastConnectedAt,
|
|
3765
|
+
lastError: conn.lastError,
|
|
3766
|
+
sessionStuckReason: conn.sessionStuckReason,
|
|
3767
|
+
terminalReason: conn.terminalReason
|
|
3768
|
+
}));
|
|
3769
|
+
}
|
|
3770
|
+
function getSocket(accountId) {
|
|
3771
|
+
return connections.get(accountId)?.sock ?? null;
|
|
3772
|
+
}
|
|
3773
|
+
function findConnectionByKey(conns, accountId) {
|
|
3774
|
+
return conns.find((c) => c.accountId === accountId) ?? conns.find((c) => c.platformAccountId === accountId);
|
|
3775
|
+
}
|
|
3776
|
+
function selectConnection(conns, accountId) {
|
|
3777
|
+
const presentKeys = conns.map((c) => c.accountId);
|
|
3778
|
+
const conn = findConnectionByKey(conns, accountId);
|
|
3779
|
+
if (!conn) return { ok: false, reason: "key-miss", presentKeys };
|
|
3780
|
+
if (!conn.sock) return { ok: false, reason: "disconnected", presentKeys };
|
|
3781
|
+
return { ok: true, sock: conn.sock, accountId: conn.accountId };
|
|
3782
|
+
}
|
|
3783
|
+
function resolveSocket(accountId) {
|
|
3784
|
+
return selectConnection(Array.from(connections.values()), accountId);
|
|
3785
|
+
}
|
|
3786
|
+
function selectStoreAccountKey(conns, accountId) {
|
|
3787
|
+
const conn = findConnectionByKey(conns, accountId);
|
|
3788
|
+
return conn ? conn.platformAccountId : accountId;
|
|
3789
|
+
}
|
|
3790
|
+
function resolveReadStoreKey(accountId) {
|
|
3791
|
+
return selectStoreAccountKey(Array.from(connections.values()), accountId);
|
|
3792
|
+
}
|
|
3793
|
+
function resolveInboundPersistAccount(args) {
|
|
3794
|
+
const result = args.accessResult;
|
|
3795
|
+
const account = result?.allowed && result.reason === "account-manager" && result.effectiveAccountId ? result.effectiveAccountId : args.connPlatformAccountId;
|
|
3796
|
+
warnNonUuidAccount("inbound-persist", account);
|
|
3797
|
+
return account;
|
|
3798
|
+
}
|
|
3799
|
+
function isManagerRoutedToHouse(args) {
|
|
3800
|
+
const result = args.accessResult;
|
|
3801
|
+
return result?.allowed === true && result.reason === "account-manager" && result.effectiveAccountId === args.houseAccountId;
|
|
3802
|
+
}
|
|
3803
|
+
function resolveConversationPersistAccount(sendAccountId, counterpartyPhone) {
|
|
3804
|
+
const conn = findConnectionByKey(Array.from(connections.values()), sendAccountId);
|
|
3805
|
+
if (!conn) {
|
|
3806
|
+
warnNonUuidAccount("reply-append-no-connection", sendAccountId);
|
|
3807
|
+
return sendAccountId;
|
|
3808
|
+
}
|
|
3809
|
+
const access = checkDmAccess({
|
|
3810
|
+
senderPhone: counterpartyPhone,
|
|
3811
|
+
selfPhone: conn.selfPhone ?? "",
|
|
3812
|
+
config: whatsAppConfig,
|
|
3813
|
+
// The per-number sub-config is keyed by the credential DIRNAME.
|
|
3814
|
+
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
3815
|
+
// Task 1850 — the account SCOPE is the platform UUID. The gate assigns this
|
|
3816
|
+
// to `effectiveAccountId` for every non-manager admit, and that value reaches
|
|
3817
|
+
// the graph and the spawn; the dirname is a credential path, not an account.
|
|
3818
|
+
accountId: conn.platformAccountId,
|
|
3819
|
+
isValidAccount: (id) => isValidAccountId(id)
|
|
3820
|
+
});
|
|
3821
|
+
return resolveInboundPersistAccount({
|
|
3822
|
+
accessResult: access,
|
|
3823
|
+
connPlatformAccountId: conn.platformAccountId
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
function isAnyConnected() {
|
|
3827
|
+
for (const conn of connections.values()) {
|
|
3828
|
+
if (conn.connected) return true;
|
|
3829
|
+
}
|
|
3830
|
+
return false;
|
|
3831
|
+
}
|
|
3832
|
+
function listConnectionPlatformAccountIds() {
|
|
3833
|
+
return [...connections.values()].map((conn) => conn.platformAccountId);
|
|
3834
|
+
}
|
|
3835
|
+
async function registerLoginSocket(accountId, sock, authDir) {
|
|
3836
|
+
if (!configDir) throw new Error("WhatsApp manager not initialized");
|
|
3837
|
+
let platformAccountId;
|
|
3838
|
+
let houseAccountId;
|
|
3839
|
+
try {
|
|
3840
|
+
platformAccountId = resolvePlatformAccountId();
|
|
3841
|
+
houseAccountId = resolveHouseAccountId();
|
|
3842
|
+
console.error(
|
|
3843
|
+
`[whatsapp-persist] resolved-account-id waname=${accountId} uuid=${platformAccountId} house=${houseAccountId}`
|
|
3844
|
+
);
|
|
3845
|
+
} catch (err) {
|
|
3846
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3847
|
+
console.error(
|
|
3848
|
+
`[whatsapp-persist] resolved-account-id FAIL waname=${accountId} reason=${reason}`
|
|
3849
|
+
);
|
|
3850
|
+
throw err;
|
|
3851
|
+
}
|
|
3852
|
+
await stopConnection(accountId);
|
|
3853
|
+
const conn = {
|
|
3854
|
+
accountId,
|
|
3855
|
+
platformAccountId,
|
|
3856
|
+
houseAccountId,
|
|
3857
|
+
accountName: whatsAppConfig.accounts?.[accountId]?.name,
|
|
3858
|
+
authDir,
|
|
3859
|
+
sock,
|
|
3860
|
+
connected: true,
|
|
3861
|
+
selfPhone: null,
|
|
3862
|
+
selfJid: null,
|
|
3863
|
+
selfLid: null,
|
|
3864
|
+
reconnectAttempts: 0,
|
|
3865
|
+
lastConnectedAt: Date.now(),
|
|
3866
|
+
abortController: new AbortController(),
|
|
3867
|
+
debouncer: null,
|
|
3868
|
+
lidMapping: null
|
|
3869
|
+
};
|
|
3870
|
+
applySelfIdentity(conn, sock, "register");
|
|
3871
|
+
connections.set(accountId, conn);
|
|
3872
|
+
monitorInbound(conn);
|
|
3873
|
+
try {
|
|
3874
|
+
await sock.sendPresenceUpdate("available");
|
|
3875
|
+
console.error(`${TAG16} presence set to available account=${accountId}`);
|
|
3876
|
+
} catch (err) {
|
|
3877
|
+
console.error(`${TAG16} presence update failed account=${accountId}: ${String(err)}`);
|
|
3878
|
+
}
|
|
3879
|
+
await runInitQueries(sock, {
|
|
3880
|
+
accountId,
|
|
3881
|
+
authDir,
|
|
3882
|
+
selfPhone: conn.selfPhone,
|
|
3883
|
+
attempt: 0,
|
|
3884
|
+
signal: conn.abortController.signal
|
|
3885
|
+
});
|
|
3886
|
+
watchForDisconnect(conn);
|
|
3887
|
+
attachSelfIdRefreshOnCredsUpdate(conn);
|
|
3888
|
+
console.error(`${TAG16} registered login socket for account=${accountId} phone=${conn.selfPhone ?? "unknown"}`);
|
|
3889
|
+
}
|
|
3890
|
+
function attachSelfIdRefreshOnCredsUpdate(conn) {
|
|
3891
|
+
if (!conn.sock) return;
|
|
3892
|
+
const sock = conn.sock;
|
|
3893
|
+
const handler = () => {
|
|
3894
|
+
setTimeout(() => {
|
|
3895
|
+
try {
|
|
3896
|
+
const fresh = readSelfId(conn.authDir);
|
|
3897
|
+
const lidChanged = fresh.lid !== conn.selfLid;
|
|
3898
|
+
conn.selfPhone = fresh.e164;
|
|
3899
|
+
conn.selfJid = fresh.jid;
|
|
3900
|
+
conn.selfLid = fresh.lid;
|
|
3901
|
+
const rawLidMapping = sock?.signalRepository?.lidMapping ?? null;
|
|
3902
|
+
conn.lidMapping = withSelfLidShortcut(rawLidMapping, fresh.lid, fresh.jid);
|
|
3903
|
+
if (lidChanged) {
|
|
3904
|
+
if (fresh.lid) {
|
|
3905
|
+
console.error(`[whatsapp:lid] resolved self-lid accountId=${conn.accountId} selfLid=${fresh.lid} source=creds-update`);
|
|
3906
|
+
} else {
|
|
3907
|
+
console.error(`[whatsapp:lid] FAIL self-lid-resolution accountId=${conn.accountId} reason=creds-update-cleared-lid`);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
} catch (err) {
|
|
3911
|
+
console.error(`[whatsapp:lid] FAIL self-lid-refresh accountId=${conn.accountId} reason=${String(err).slice(0, 80)}`);
|
|
3912
|
+
}
|
|
3913
|
+
}, 1e3).unref?.();
|
|
3914
|
+
};
|
|
3915
|
+
sock.ev.on("creds.update", handler);
|
|
3916
|
+
}
|
|
3917
|
+
function reloadConfig(accountConfig) {
|
|
3918
|
+
loadConfig(accountConfig);
|
|
3919
|
+
}
|
|
3920
|
+
function getMessages(accountId, jid, limit) {
|
|
3921
|
+
const entries = messageStore.get(messageStoreKey(accountId, jid)) ?? [];
|
|
3922
|
+
return limit ? entries.slice(-limit) : entries.slice();
|
|
3923
|
+
}
|
|
3924
|
+
function listConversationJids(accountId) {
|
|
3925
|
+
const prefix = messageStoreKey(accountId, "");
|
|
3926
|
+
const jids = [];
|
|
3927
|
+
for (const key of messageStore.keys()) {
|
|
3928
|
+
if (key.startsWith(prefix)) {
|
|
3929
|
+
jids.push(key.slice(prefix.length));
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
return jids;
|
|
3933
|
+
}
|
|
3934
|
+
function listStoreAccountKeys() {
|
|
3935
|
+
const accounts = /* @__PURE__ */ new Set();
|
|
3936
|
+
for (const key of messageStore.keys()) {
|
|
3937
|
+
const i = key.indexOf(":");
|
|
3938
|
+
if (i > 0) accounts.add(key.slice(0, i));
|
|
3939
|
+
}
|
|
3940
|
+
return [...accounts];
|
|
3941
|
+
}
|
|
3942
|
+
async function shutdown() {
|
|
3943
|
+
await drainCredsSaveQueue();
|
|
3944
|
+
const ids = Array.from(connections.keys());
|
|
3945
|
+
await Promise.all(ids.map((id) => stopConnection(id)));
|
|
3946
|
+
initialized = false;
|
|
3947
|
+
console.error(`${TAG16} shutdown complete`);
|
|
3948
|
+
}
|
|
3949
|
+
function loadConfig(accountConfig) {
|
|
3950
|
+
try {
|
|
3951
|
+
const raw = accountConfig?.whatsapp;
|
|
3952
|
+
if (raw && typeof raw === "object") {
|
|
3953
|
+
const wa = raw;
|
|
3954
|
+
const result = parseWhatsAppConfigTolerant(wa);
|
|
3955
|
+
if (result.ok) {
|
|
3956
|
+
whatsAppConfig = result.data;
|
|
3957
|
+
if (result.droppedKeys.length > 0) {
|
|
3958
|
+
console.error(`${TAG16} config: dropped unknown keys=[${result.droppedKeys.join(",")}] preserved known config`);
|
|
3959
|
+
}
|
|
3960
|
+
} else {
|
|
3961
|
+
console.error(`${TAG16} config validation failed: ${result.error.message}`);
|
|
3962
|
+
whatsAppConfig = {};
|
|
3963
|
+
}
|
|
3964
|
+
const diskAdmin = Array.isArray(wa.adminPhones) ? wa.adminPhones : [];
|
|
3965
|
+
if (diskAdmin.length > 0 && (whatsAppConfig.adminPhones?.length ?? 0) === 0) {
|
|
3966
|
+
console.error(`${TAG16} WARN adminPhones present on disk but empty after load`);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
} catch (err) {
|
|
3970
|
+
console.error(`${TAG16} config load error: ${String(err)}`);
|
|
3971
|
+
whatsAppConfig = {};
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
async function connectWithReconnect(conn) {
|
|
3975
|
+
const maxAttempts = MAX_RECONNECT_ATTEMPTS;
|
|
3976
|
+
while (!conn.abortController.signal.aborted) {
|
|
3977
|
+
let uptimeMs = 0;
|
|
3978
|
+
let cycleError = null;
|
|
3979
|
+
let connectedAt;
|
|
3980
|
+
try {
|
|
3981
|
+
console.error(`${TAG16} connecting account=${conn.accountId} attempt=${conn.reconnectAttempts}`);
|
|
3982
|
+
if (conn.debouncer) {
|
|
3983
|
+
await conn.debouncer.destroy();
|
|
3984
|
+
conn.debouncer = null;
|
|
3985
|
+
}
|
|
3986
|
+
const sock = await createWaSocket({
|
|
3987
|
+
authDir: conn.authDir,
|
|
3988
|
+
silent: true,
|
|
3989
|
+
account: conn.accountId
|
|
3990
|
+
});
|
|
3991
|
+
conn.sock = sock;
|
|
3992
|
+
applySelfIdentity(conn, sock, "pre-listener");
|
|
3993
|
+
monitorInbound(conn);
|
|
3994
|
+
console.error(`${TAG16} socket created account=${conn.accountId} \u2014 waiting for connection`);
|
|
3995
|
+
await waitForConnection(sock);
|
|
3996
|
+
connectedAt = Date.now();
|
|
3997
|
+
conn.connected = true;
|
|
3998
|
+
conn.lastConnectedAt = connectedAt;
|
|
3999
|
+
conn.lastError = void 0;
|
|
4000
|
+
conn.terminalReason = void 0;
|
|
4001
|
+
applySelfIdentity(conn, sock, "post-connect");
|
|
4002
|
+
console.error(`${TAG16} connected account=${conn.accountId} phone=${conn.selfPhone ?? "unknown"}`);
|
|
4003
|
+
try {
|
|
4004
|
+
await sock.sendPresenceUpdate("available");
|
|
4005
|
+
console.error(`${TAG16} presence set to available account=${conn.accountId}`);
|
|
4006
|
+
} catch (err) {
|
|
4007
|
+
console.error(`${TAG16} presence update failed account=${conn.accountId}: ${String(err)}`);
|
|
4008
|
+
}
|
|
4009
|
+
await runInitQueries(sock, {
|
|
4010
|
+
accountId: conn.accountId,
|
|
4011
|
+
authDir: conn.authDir,
|
|
4012
|
+
selfPhone: conn.selfPhone,
|
|
4013
|
+
attempt: conn.reconnectAttempts,
|
|
4014
|
+
signal: conn.abortController.signal
|
|
4015
|
+
});
|
|
4016
|
+
attachSelfIdRefreshOnCredsUpdate(conn);
|
|
4017
|
+
await waitForDisconnectEvent(conn);
|
|
4018
|
+
uptimeMs = Date.now() - connectedAt;
|
|
4019
|
+
conn.connected = false;
|
|
4020
|
+
conn.sock = null;
|
|
4021
|
+
} catch (err) {
|
|
4022
|
+
conn.connected = false;
|
|
4023
|
+
conn.sock = null;
|
|
4024
|
+
cycleError = err;
|
|
4025
|
+
if (connectedAt) {
|
|
4026
|
+
uptimeMs = Date.now() - connectedAt;
|
|
4027
|
+
}
|
|
4028
|
+
const classification = classifyDisconnect(err);
|
|
4029
|
+
conn.lastError = classification.message;
|
|
4030
|
+
console.error(`${TAG16} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
|
|
4031
|
+
if (!classification.shouldRetry) {
|
|
4032
|
+
conn.terminalReason = classification.message;
|
|
4033
|
+
console.error(`${TAG16} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
const decision = computeReconnectNextState({
|
|
4038
|
+
reconnectAttempts: conn.reconnectAttempts,
|
|
4039
|
+
uptimeMs,
|
|
4040
|
+
stableThresholdMs: STABLE_UPTIME_MS,
|
|
4041
|
+
maxAttempts
|
|
4042
|
+
});
|
|
4043
|
+
if (decision.action === "abort") {
|
|
4044
|
+
const stuckReason = `GIVING UP account=${conn.accountId} attempts=${decision.finalAttempts}/${maxAttempts} uptimeMsLast=${uptimeMs} stableThresholdMs=${STABLE_UPTIME_MS} lastError=${conn.lastError ?? "(none)"}`;
|
|
4045
|
+
console.error(
|
|
4046
|
+
`${TAG16} ${stuckReason} \u2014 re-pair via QR or restart required; WhatsApp will not reconnect automatically`
|
|
4047
|
+
);
|
|
4048
|
+
conn.sessionStuckReason = stuckReason;
|
|
4049
|
+
conn.lastError = stuckReason;
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
conn.reconnectAttempts = decision.nextAttempts;
|
|
4053
|
+
if (decision.reason === "short-lived") {
|
|
4054
|
+
console.error(
|
|
4055
|
+
`${TAG16} short-lived session account=${conn.accountId} uptimeMs=${uptimeMs} attempt=${decision.nextAttempts}/${maxAttempts} lastError=${conn.lastError ?? "(clean disconnect)"}`
|
|
4056
|
+
);
|
|
4057
|
+
} else {
|
|
4058
|
+
console.error(
|
|
4059
|
+
`${TAG16} session stable account=${conn.accountId} uptimeMs=${uptimeMs} \u2014 reconnect counter reset`
|
|
4060
|
+
);
|
|
4061
|
+
}
|
|
4062
|
+
if (decision.reason === "short-lived" || cycleError) {
|
|
4063
|
+
const delay = computeBackoff(Math.max(1, decision.nextAttempts));
|
|
4064
|
+
console.error(
|
|
4065
|
+
`${TAG16} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
4066
|
+
);
|
|
4067
|
+
await new Promise((resolve6) => {
|
|
4068
|
+
const timer = setTimeout(resolve6, delay);
|
|
4069
|
+
conn.abortController.signal.addEventListener("abort", () => {
|
|
4070
|
+
clearTimeout(timer);
|
|
4071
|
+
resolve6();
|
|
4072
|
+
}, { once: true });
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
if (conn.abortController.signal.aborted) return;
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
function waitForDisconnectEvent(conn) {
|
|
4079
|
+
return new Promise((resolve6) => {
|
|
4080
|
+
if (!conn.sock) {
|
|
4081
|
+
resolve6();
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
const sock = conn.sock;
|
|
4085
|
+
const handler = (update) => {
|
|
4086
|
+
if (update.connection === "close") {
|
|
4087
|
+
sock.ev.off("connection.update", handler);
|
|
4088
|
+
resolve6();
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
sock.ev.on("connection.update", handler);
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
function watchForDisconnect(conn) {
|
|
4095
|
+
if (!conn.sock) return;
|
|
4096
|
+
conn.sock.ev.on("connection.update", (update) => {
|
|
4097
|
+
if (update.connection === "close") {
|
|
4098
|
+
if (connections.get(conn.accountId) !== conn) return;
|
|
4099
|
+
console.error(`${TAG16} socket disconnected for account=${conn.accountId}`);
|
|
4100
|
+
conn.connected = false;
|
|
4101
|
+
conn.sock = null;
|
|
4102
|
+
connectWithReconnect(conn).catch((err) => {
|
|
4103
|
+
console.error(`${TAG16} reconnection failed for account=${conn.accountId}: ${formatError(err)}`);
|
|
4104
|
+
});
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
function applySelfIdentity(conn, sock, phase) {
|
|
4109
|
+
const selfId = readSelfId(conn.authDir);
|
|
4110
|
+
conn.selfPhone = selfId.e164;
|
|
4111
|
+
conn.selfJid = selfId.jid;
|
|
4112
|
+
conn.selfLid = selfId.lid;
|
|
4113
|
+
const rawLidMapping = sock.signalRepository?.lidMapping ?? null;
|
|
4114
|
+
conn.lidMapping = withSelfLidShortcut(rawLidMapping, selfId.lid, selfId.jid);
|
|
4115
|
+
if (selfId.lid) {
|
|
4116
|
+
console.error(
|
|
4117
|
+
`[whatsapp:lid] resolved self-lid accountId=${conn.accountId} selfLid=${selfId.lid} source=creds phase=${phase}`
|
|
4118
|
+
);
|
|
4119
|
+
} else {
|
|
4120
|
+
console.error(
|
|
4121
|
+
`[whatsapp:lid] FAIL self-lid-resolution accountId=${conn.accountId} reason=creds-missing-lid phase=${phase}`
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
function monitorInbound(conn) {
|
|
4126
|
+
if (!conn.sock || !onInboundMessage) return;
|
|
4127
|
+
const sock = conn.sock;
|
|
4128
|
+
const debounceMs = whatsAppConfig.accounts?.[conn.accountId]?.debounceMs ?? whatsAppConfig.debounceMs ?? 0;
|
|
4129
|
+
console.error(`${TAG16} monitorInbound started account=${conn.accountId} debounceMs=${debounceMs}`);
|
|
4130
|
+
conn.debouncer = createInboundDebouncer({
|
|
4131
|
+
debounceMs,
|
|
4132
|
+
buildKey: (payload) => {
|
|
4133
|
+
const conversationKey = payload.isGroup ? payload.groupJid : payload.senderPhone;
|
|
4134
|
+
return `${payload.accountId}:${conversationKey ?? payload.senderPhone}:${payload.senderPhone}`;
|
|
4135
|
+
},
|
|
4136
|
+
onFlush: (entries) => {
|
|
4137
|
+
if (!onInboundMessage) return;
|
|
4138
|
+
if (entries.length > 1) {
|
|
4139
|
+
console.error(`${TAG16} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
|
|
4140
|
+
}
|
|
4141
|
+
dispatchInboundPayload(conn, combineInboundBatch(entries));
|
|
4142
|
+
},
|
|
4143
|
+
onError: (err) => {
|
|
4144
|
+
console.error(`${TAG16} debounce flush error account=${conn.accountId}: ${String(err)}`);
|
|
4145
|
+
}
|
|
4146
|
+
});
|
|
4147
|
+
sock.ev.on("messages.upsert", async (upsert) => {
|
|
4148
|
+
if (upsert.type !== "notify" && upsert.type !== "append") return;
|
|
4149
|
+
for (const msg of upsert.messages) {
|
|
4150
|
+
try {
|
|
4151
|
+
const remoteJid = msg.key.remoteJid;
|
|
4152
|
+
if (!remoteJid || remoteJid === "status@broadcast") continue;
|
|
4153
|
+
if (!msg.message) continue;
|
|
4154
|
+
let receiptDeferredToWorker = false;
|
|
4155
|
+
let dmAccess = null;
|
|
4156
|
+
const extracted = extractMessage(msg);
|
|
4157
|
+
const mediaServable = !!(extracted.mediaType && extracted.mimetype);
|
|
4158
|
+
if (!extracted.text && !mediaServable && msg.key.id) {
|
|
4159
|
+
markDeclined({
|
|
4160
|
+
msgId: msg.key.id,
|
|
4161
|
+
account: conn.accountId,
|
|
4162
|
+
reason: "no-persistable-content"
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
if (extracted.text || mediaServable) {
|
|
4166
|
+
const isGroup = isGroupJid(remoteJid);
|
|
4167
|
+
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
4168
|
+
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
4169
|
+
const ts = msg.messageTimestamp ? Number(msg.messageTimestamp) : Math.floor(Date.now() / 1e3);
|
|
4170
|
+
const fromMe = Boolean(msg.key.fromMe);
|
|
4171
|
+
dmAccess = isGroup ? null : checkDmAccess({
|
|
4172
|
+
senderPhone,
|
|
4173
|
+
selfPhone: conn.selfPhone ?? "",
|
|
4174
|
+
config: whatsAppConfig,
|
|
4175
|
+
// Keyed by the credential DIRNAME — a per-number sub-config.
|
|
4176
|
+
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
4177
|
+
// Task 1850 — the account SCOPE is the platform UUID, never the
|
|
4178
|
+
// dirname. See resolveConversationPersistAccount.
|
|
4179
|
+
accountId: conn.platformAccountId,
|
|
4180
|
+
isValidAccount: (id) => isValidAccountId(id)
|
|
4181
|
+
});
|
|
4182
|
+
const persistAccountId = resolveInboundPersistAccount({
|
|
4183
|
+
accessResult: dmAccess,
|
|
4184
|
+
connPlatformAccountId: conn.platformAccountId
|
|
4185
|
+
});
|
|
4186
|
+
const managerScoped = dmAccess?.allowed === true && dmAccess.reason === "account-manager";
|
|
4187
|
+
{
|
|
4188
|
+
const notifyDecision = decideWaNotify({
|
|
4189
|
+
upsertType: upsert.type,
|
|
4190
|
+
fromMe,
|
|
4191
|
+
msgKeyId: msg.key.id,
|
|
4192
|
+
// This block only runs when the persist gate at :1286 passed.
|
|
4193
|
+
hasPersistableContent: true,
|
|
4194
|
+
// A callback, not a boolean: `isNotifyDuplicate` marks the id it
|
|
4195
|
+
// is asked about, and the gate calls this last so only a message
|
|
4196
|
+
// that actually notifies burns its id.
|
|
4197
|
+
isDuplicate: () => msg.key.id ? isNotifyDuplicate(msg.key.id) : false,
|
|
4198
|
+
access: dmAccess
|
|
4199
|
+
});
|
|
4200
|
+
if (msg.key.id && !(notifyDecision.emit === false && notifyDecision.reason === "duplicate")) {
|
|
4201
|
+
appendNotifyEligible({
|
|
4202
|
+
msgId: msg.key.id,
|
|
4203
|
+
account: conn.accountId,
|
|
4204
|
+
upsertType: upsert.type,
|
|
4205
|
+
fromMe,
|
|
4206
|
+
allowed: dmAccess?.allowed === true,
|
|
4207
|
+
reason: dmAccess?.reason ?? "group",
|
|
4208
|
+
passive: dmAccess?.passive === true,
|
|
4209
|
+
persistAccount: persistAccountId
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
if (notifyDecision.emit && msg.key.id) {
|
|
4213
|
+
console.error(
|
|
4214
|
+
`[wa-notify] op=eligible msgId=${msg.key.id} account=${persistAccountId} senderType=${notifyDecision.senderType} reason=${dmAccess?.reason ?? "none"} passive=${dmAccess?.passive === true}`
|
|
4215
|
+
);
|
|
4216
|
+
if (onNotify) {
|
|
4217
|
+
try {
|
|
4218
|
+
onNotify(
|
|
4219
|
+
buildNotifyPending({
|
|
4220
|
+
msgKeyId: msg.key.id,
|
|
4221
|
+
pushName: msg.pushName ?? void 0,
|
|
4222
|
+
senderPhone,
|
|
4223
|
+
text: extracted.text,
|
|
4224
|
+
mediaType: extracted.mediaType,
|
|
4225
|
+
senderType: notifyDecision.senderType,
|
|
4226
|
+
persistAccountId,
|
|
4227
|
+
ledgerAccount: conn.accountId,
|
|
4228
|
+
ts
|
|
4229
|
+
})
|
|
4230
|
+
);
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
console.error(
|
|
4233
|
+
`[wa-notify] op=emit-failed msgId=${msg.key.id} detail=${err instanceof Error ? err.message : String(err)}`
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
} else if (!notifyDecision.emit) {
|
|
4238
|
+
console.error(
|
|
4239
|
+
`[wa-notify] op=skip msgId=${msg.key.id ?? "none"} reason=${notifyDecision.reason}`
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
const storeKey = messageStoreKey(persistAccountId, remoteJid);
|
|
4244
|
+
if (extracted.text) {
|
|
4245
|
+
storeMessage(storeKey, {
|
|
4246
|
+
id: msg.key.id ?? `${remoteJid}-${ts}`,
|
|
4247
|
+
sender: senderPhone,
|
|
4248
|
+
senderName: msg.pushName ?? void 0,
|
|
4249
|
+
body: extracted.text,
|
|
4250
|
+
timestamp: ts,
|
|
4251
|
+
fromMe: Boolean(msg.key.fromMe),
|
|
4252
|
+
quoted: extracted.quotedMessage ? { id: extracted.quotedMessage.id, body: extracted.quotedMessage.text, sender: extracted.quotedMessage.sender } : void 0
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
const entries = messageStore.get(storeKey);
|
|
4256
|
+
console.error(
|
|
4257
|
+
`${TAG16} stored message ${msg.key.id ?? "?"} for ${remoteJid} (type: ${upsert.type}, store size: ${entries?.length ?? 0}/${MESSAGE_STORE_MAX}) storeAccount=${persistAccountId} socketOwner=${conn.platformAccountId}`
|
|
4258
|
+
);
|
|
4259
|
+
recordActivity({
|
|
4260
|
+
accountId: conn.accountId,
|
|
4261
|
+
direction: "inbound",
|
|
4262
|
+
jid: remoteJid
|
|
4263
|
+
});
|
|
4264
|
+
const cacheKeyAgentType = isGroup ? "public" : fromMe ? "admin" : dmAccess?.agentType ?? "public";
|
|
4265
|
+
const cacheKey = deriveCacheKey({
|
|
4266
|
+
agentType: cacheKeyAgentType,
|
|
4267
|
+
// Task 1786 — an account-manager keys on its bound sub-account, so a
|
|
4268
|
+
// manager gets its own `:Conversation` instead of merging into the
|
|
4269
|
+
// owner's bare `whatsapp:<dirname>` owner-mirror row. `deriveCacheKey`
|
|
4270
|
+
// returns `whatsapp:<accountId>` for admin; a UUID contains no colon,
|
|
4271
|
+
// so the key stays 2 segments and `deriveScope` still reads "admin".
|
|
4272
|
+
// Every non-manager case passes conn.accountId, byte-identical.
|
|
4273
|
+
accountId: managerScoped ? persistAccountId : conn.accountId,
|
|
4274
|
+
senderPhone,
|
|
4275
|
+
isGroup,
|
|
4276
|
+
groupJid: isGroup ? remoteJid : void 0,
|
|
4277
|
+
isOwnerMirror: fromMe && !isGroup
|
|
4278
|
+
});
|
|
4279
|
+
if (msg.key.id) {
|
|
4280
|
+
const merged = await ensureWhatsAppConversation({
|
|
4281
|
+
accountId: conn.accountId,
|
|
4282
|
+
platformAccountId: persistAccountId,
|
|
4283
|
+
// Task 1786 — the socket owner, logged beside the stamped account
|
|
4284
|
+
// so a divergence between the two is one greppable line.
|
|
4285
|
+
connectionAccountId: conn.platformAccountId,
|
|
4286
|
+
cacheKey,
|
|
4287
|
+
agentType: cacheKeyAgentType,
|
|
4288
|
+
groupJid: isGroup ? remoteJid : void 0
|
|
4289
|
+
});
|
|
4290
|
+
if (merged) {
|
|
4291
|
+
const { canonical: canonicalRemoteJid } = await resolveCanonicalConversationJid(
|
|
4292
|
+
remoteJid,
|
|
4293
|
+
conn.lidMapping
|
|
4294
|
+
);
|
|
4295
|
+
const alreadyRecorded = fromMe && echoAlreadyRecorded({
|
|
4296
|
+
readConversation,
|
|
4297
|
+
// Task 1786 — read the account /reply now APPENDS under. The
|
|
4298
|
+
// echo gate asks "did /reply already store this?", so it must
|
|
4299
|
+
// look in the same store file the reply landed in; reading the
|
|
4300
|
+
// socket owner's file would report every manager echo as
|
|
4301
|
+
// unrecorded and store a duplicate copy.
|
|
4302
|
+
platformAccountId: persistAccountId,
|
|
4303
|
+
canonicalRemoteJid,
|
|
4304
|
+
accountId: conn.accountId,
|
|
4305
|
+
remoteJid,
|
|
4306
|
+
msgKeyId: msg.key.id
|
|
4307
|
+
});
|
|
4308
|
+
const wantsServableMedia = mediaServable && !!conn.sock && (!fromMe || !alreadyRecorded);
|
|
4309
|
+
if (fromMe && mediaServable && !!conn.sock) {
|
|
4310
|
+
console.error(
|
|
4311
|
+
`${TAG16} fromMe-media-echo msgKey=${msg.key.id} decision=${alreadyRecorded ? "skip" : "store"} reason=${alreadyRecorded ? "already-recorded" : "no-prior-record"}`
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
const mediaSeed = `whatsapp-live:${conn.accountId}:${canonicalRemoteJid}:${msg.key.id}`;
|
|
4315
|
+
const maxBytes = echoStoreMaxBytes({
|
|
4316
|
+
fromMe,
|
|
4317
|
+
mediaMaxMb: whatsAppConfig.mediaMaxMb
|
|
4318
|
+
});
|
|
4319
|
+
const basePersist = {
|
|
4320
|
+
accountId: conn.accountId,
|
|
4321
|
+
platformAccountId: persistAccountId,
|
|
4322
|
+
remoteJid,
|
|
4323
|
+
canonicalRemoteJid,
|
|
4324
|
+
cacheKey,
|
|
4325
|
+
sessionId: merged.sessionId,
|
|
4326
|
+
msgKeyId: msg.key.id,
|
|
4327
|
+
fromMe,
|
|
4328
|
+
senderPhone,
|
|
4329
|
+
selfPhone: conn.selfPhone,
|
|
4330
|
+
timestamp: ts,
|
|
4331
|
+
pushName: msg.pushName ?? void 0,
|
|
4332
|
+
quoted: extracted.quotedMessage ? {
|
|
4333
|
+
id: extracted.quotedMessage.id,
|
|
4334
|
+
body: extracted.quotedMessage.text,
|
|
4335
|
+
sender: extracted.quotedMessage.sender
|
|
4336
|
+
} : void 0
|
|
4337
|
+
};
|
|
4338
|
+
if (wantsServableMedia && upsert.type === "append") {
|
|
4339
|
+
const sock2 = conn.sock;
|
|
4340
|
+
const platformAccountId = persistAccountId;
|
|
4341
|
+
const caption = extracted.caption ?? "";
|
|
4342
|
+
const fallbackBody = mediaFallbackBody(extracted);
|
|
4343
|
+
const msgKeyId = msg.key.id;
|
|
4344
|
+
const participant = isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0;
|
|
4345
|
+
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
4346
|
+
const sendAppendReceipt = () => {
|
|
4347
|
+
if (sendReceipts && msgKeyId) {
|
|
4348
|
+
sendReadReceipt(sock2, remoteJid, [msgKeyId], participant, conn.accountId);
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
const accepted = appendMediaQueue.schedule(async () => {
|
|
4352
|
+
const id = await storeInboundMediaServable(msg, sock2, platformAccountId, mediaSeed, { maxBytes }) ?? void 0;
|
|
4353
|
+
const persisted = await persistWhatsAppMessage({
|
|
4354
|
+
...basePersist,
|
|
4355
|
+
body: id ? caption : fallbackBody,
|
|
4356
|
+
attachmentId: id
|
|
4357
|
+
});
|
|
4358
|
+
if (persisted) sendAppendReceipt();
|
|
4359
|
+
});
|
|
4360
|
+
receiptDeferredToWorker = true;
|
|
4361
|
+
if (!accepted) {
|
|
4362
|
+
console.error(
|
|
4363
|
+
`${TAG16} append-media overflow account=${conn.platformAccountId} msgKey=${msgKeyId ?? "?"} \u2014 pending cap ${APPEND_MEDIA_MAX_PENDING} reached, text-fallback persist, inline media shed`
|
|
4364
|
+
);
|
|
4365
|
+
const persisted = await persistWhatsAppMessage({
|
|
4366
|
+
...basePersist,
|
|
4367
|
+
body: fallbackBody,
|
|
4368
|
+
attachmentId: void 0
|
|
4369
|
+
});
|
|
4370
|
+
if (persisted) sendAppendReceipt();
|
|
4371
|
+
}
|
|
4372
|
+
} else {
|
|
4373
|
+
let mediaAttachmentId;
|
|
4374
|
+
if (wantsServableMedia) {
|
|
4375
|
+
mediaAttachmentId = await storeInboundMediaServable(
|
|
4376
|
+
msg,
|
|
4377
|
+
conn.sock,
|
|
4378
|
+
// Task 1786 — attachment bytes follow the record's account.
|
|
4379
|
+
persistAccountId,
|
|
4380
|
+
mediaSeed,
|
|
4381
|
+
{ maxBytes }
|
|
4382
|
+
) ?? void 0;
|
|
4383
|
+
}
|
|
4384
|
+
await persistWhatsAppMessage({
|
|
4385
|
+
...basePersist,
|
|
4386
|
+
body: mediaAttachmentId ? extracted.caption ?? "" : extracted.text,
|
|
4387
|
+
attachmentId: mediaAttachmentId
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
if (upsert.type === "append") {
|
|
4394
|
+
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
4395
|
+
if (sendReceipts && msg.key.id && !receiptDeferredToWorker) {
|
|
4396
|
+
sendReadReceipt(
|
|
4397
|
+
sock,
|
|
4398
|
+
remoteJid,
|
|
4399
|
+
[msg.key.id],
|
|
4400
|
+
isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0,
|
|
4401
|
+
conn.accountId
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
4404
|
+
const decision = evaluateSelfChatCommand({
|
|
4405
|
+
accountId: conn.accountId,
|
|
4406
|
+
remoteJid,
|
|
4407
|
+
fromMe: Boolean(msg.key.fromMe),
|
|
4408
|
+
msgId: msg.key.id ?? null,
|
|
4409
|
+
messageTimestampSec: msg.messageTimestamp ? Number(msg.messageTimestamp) : null,
|
|
4410
|
+
lastConnectedAtMs: conn.lastConnectedAt ?? null,
|
|
4411
|
+
selfJid: conn.selfJid,
|
|
4412
|
+
selfLid: conn.selfLid,
|
|
4413
|
+
selfPhone: conn.selfPhone,
|
|
4414
|
+
text: extracted.text || null
|
|
4415
|
+
});
|
|
4416
|
+
if (decision.kind === "dispatch" && onInboundMessage) {
|
|
4417
|
+
const reply = async (text) => {
|
|
4418
|
+
const currentSock = conn.sock;
|
|
4419
|
+
if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
|
|
4420
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
4421
|
+
};
|
|
4422
|
+
const composing = async () => {
|
|
4423
|
+
const currentSock = conn.sock;
|
|
4424
|
+
if (currentSock) await sendComposing(currentSock, remoteJid);
|
|
4425
|
+
};
|
|
4426
|
+
dispatchInboundPayload(conn, buildSelfChatDispatch({
|
|
4427
|
+
accountId: conn.accountId,
|
|
4428
|
+
platformAccountId: conn.platformAccountId,
|
|
4429
|
+
selfPhone: conn.selfPhone,
|
|
4430
|
+
remoteJid,
|
|
4431
|
+
text: decision.text,
|
|
4432
|
+
reply,
|
|
4433
|
+
composing
|
|
4434
|
+
}));
|
|
4435
|
+
continue;
|
|
4436
|
+
}
|
|
4437
|
+
const skippedSenderJid = isGroupJid(remoteJid) ? msg.key.participant ?? remoteJid : remoteJid;
|
|
4438
|
+
const skippedSenderPhone = await resolveJidToE164(skippedSenderJid, conn.lidMapping) ?? skippedSenderJid;
|
|
4439
|
+
console.error(
|
|
4440
|
+
`${TAG16} append-type message ${msg.key.id ?? "?"} dispatch skipped from=${skippedSenderPhone}${msg.pushName ? ` name=${msg.pushName}` : ""} account=${conn.accountId} (persistence not confirmed here)`
|
|
4441
|
+
);
|
|
4442
|
+
continue;
|
|
4443
|
+
}
|
|
4444
|
+
await handleInboundMessage(conn, msg);
|
|
4445
|
+
} catch (err) {
|
|
4446
|
+
console.error(`${TAG16} inbound handler error account=${conn.accountId}: ${String(err)}`);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
});
|
|
4450
|
+
console.error(`[whatsapp-reaction] op=subscribed account=${conn.platformAccountId}`);
|
|
4451
|
+
sock.ev.on("messages.reaction", async (reactions) => {
|
|
4452
|
+
for (const entry of reactions) {
|
|
4453
|
+
try {
|
|
4454
|
+
const sig = readReactionEntry(entry);
|
|
4455
|
+
if (!sig) continue;
|
|
4456
|
+
if (sig.fromMe) continue;
|
|
4457
|
+
const reactorTelephone = await resolveJidToE164(sig.reactorJid, conn.lidMapping) ?? sig.reactorJid;
|
|
4458
|
+
const { canonical: canonicalRemoteJid } = await resolveCanonicalConversationJid(
|
|
4459
|
+
sig.chatJid,
|
|
4460
|
+
conn.lidMapping
|
|
4461
|
+
);
|
|
4462
|
+
console.error(
|
|
4463
|
+
`[whatsapp-reaction] op=recv account=${conn.platformAccountId} target=${sig.targetMsgKeyId} reactor=${reactorTelephone} emoji=${sig.emoji || "EMPTY"} group=${sig.isGroup}`
|
|
4464
|
+
);
|
|
4465
|
+
const convo = readConversation(conn.platformAccountId, canonicalRemoteJid);
|
|
4466
|
+
const target = convo.find((m) => m.msgKeyId === sig.targetMsgKeyId);
|
|
4467
|
+
const reactorName = target && !target.fromMe && target.senderTelephone === reactorTelephone ? target.senderName : convo.find((m) => m.senderTelephone === reactorTelephone && m.senderName)?.senderName ?? null;
|
|
4468
|
+
if (target) {
|
|
4469
|
+
console.error(`[whatsapp-reaction] op=target-ok account=${conn.platformAccountId} target=${sig.targetMsgKeyId}`);
|
|
4470
|
+
} else {
|
|
4471
|
+
console.error(`[whatsapp-reaction] op=orphan account=${conn.platformAccountId} target=${sig.targetMsgKeyId} reason=target-not-found`);
|
|
4472
|
+
}
|
|
4473
|
+
const dateSent = sig.timestampMs ? new Date(sig.timestampMs).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
4474
|
+
const { appended } = appendReaction(conn.platformAccountId, {
|
|
4475
|
+
kind: "reaction",
|
|
4476
|
+
targetMsgKeyId: sig.targetMsgKeyId,
|
|
4477
|
+
reactorTelephone,
|
|
4478
|
+
reactorName,
|
|
4479
|
+
emoji: sig.emoji,
|
|
4480
|
+
removed: sig.removed,
|
|
4481
|
+
dateSent,
|
|
4482
|
+
remoteJid: canonicalRemoteJid
|
|
4483
|
+
});
|
|
4484
|
+
if (appended) {
|
|
4485
|
+
console.error(
|
|
4486
|
+
`[whatsapp-reaction] op=persist account=${conn.platformAccountId} target=${sig.targetMsgKeyId} reactor=${reactorTelephone} emoji=${sig.emoji || "EMPTY"} removed=${sig.removed}`
|
|
4487
|
+
);
|
|
4488
|
+
} else {
|
|
4489
|
+
console.error(`[whatsapp-reaction] op=persist-fail account=${conn.platformAccountId} target=${sig.targetMsgKeyId} reason=append-failed`);
|
|
4490
|
+
}
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
console.error(`[whatsapp-reaction] op=persist-fail account=${conn.platformAccountId} reason=${String(err).slice(0, 120)}`);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4497
|
+
async function handleInboundMessage(conn, msg) {
|
|
4498
|
+
if (!conn.sock || !onInboundMessage) return;
|
|
4499
|
+
const remoteJid = msg.key.remoteJid;
|
|
4500
|
+
if (!remoteJid) return;
|
|
4501
|
+
if (remoteJid === "status@broadcast") {
|
|
4502
|
+
console.error(`${TAG16} drop: status broadcast account=${conn.accountId}`);
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
if (!msg.message) {
|
|
4506
|
+
console.error(`${TAG16} drop: empty message account=${conn.accountId} from=${remoteJid}`);
|
|
4507
|
+
return;
|
|
4508
|
+
}
|
|
4509
|
+
const dedupKey = `${conn.accountId}:${remoteJid}:${msg.key.id}`;
|
|
4510
|
+
if (isDuplicateInbound(dedupKey)) {
|
|
4511
|
+
console.error(`${TAG16} drop: duplicate account=${conn.accountId} key=${dedupKey}`);
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
if (msg.key.fromMe) {
|
|
4515
|
+
if (msg.key.id && isAgentSentMessage(msg.key.id)) {
|
|
4516
|
+
console.error(`${TAG16} drop: echo suppression account=${conn.accountId} msgId=${msg.key.id}`);
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const extracted2 = extractMessage(msg);
|
|
4520
|
+
if (!extracted2.text) {
|
|
4521
|
+
console.error(`${TAG16} owner reply skipped \u2014 no text content account=${conn.accountId}`);
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
const isGroup2 = isGroupJid(remoteJid);
|
|
4525
|
+
const senderPhone2 = conn.selfPhone ?? "owner";
|
|
4526
|
+
console.error(`${TAG16} owner reply mirrored to session from=${senderPhone2} account=${conn.accountId}`);
|
|
4527
|
+
const reply2 = async (text) => {
|
|
4528
|
+
const currentSock = conn.sock;
|
|
4529
|
+
if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
|
|
4530
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
4531
|
+
};
|
|
4532
|
+
const composing2 = async () => {
|
|
4533
|
+
const currentSock = conn.sock;
|
|
4534
|
+
if (currentSock) await sendComposing(currentSock, remoteJid);
|
|
4535
|
+
};
|
|
4536
|
+
dispatchInboundPayload(conn, {
|
|
4537
|
+
accountId: conn.accountId,
|
|
4538
|
+
agentType: "admin",
|
|
4539
|
+
senderPhone: senderPhone2,
|
|
4540
|
+
text: `[Owner reply] ${extracted2.text}`,
|
|
4541
|
+
isGroup: isGroup2,
|
|
4542
|
+
groupJid: isGroup2 ? remoteJid : void 0,
|
|
4543
|
+
reply: reply2,
|
|
4544
|
+
composing: composing2,
|
|
4545
|
+
cacheKey: deriveCacheKey({
|
|
4546
|
+
agentType: "admin",
|
|
4547
|
+
accountId: conn.accountId,
|
|
4548
|
+
senderPhone: senderPhone2,
|
|
4549
|
+
isGroup: isGroup2,
|
|
4550
|
+
groupJid: isGroup2 ? remoteJid : void 0,
|
|
4551
|
+
isOwnerMirror: true
|
|
4552
|
+
}),
|
|
4553
|
+
media: [],
|
|
4554
|
+
isOwnerMirror: true
|
|
4555
|
+
});
|
|
4556
|
+
return;
|
|
4557
|
+
}
|
|
4558
|
+
const extracted = extractMessage(msg);
|
|
4559
|
+
if (!extracted.text && !extracted.mediaType) {
|
|
4560
|
+
console.error(`${TAG16} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
|
|
4561
|
+
return;
|
|
4562
|
+
}
|
|
4563
|
+
const isGroup = isGroupJid(remoteJid);
|
|
4564
|
+
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
4565
|
+
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
4566
|
+
const lidCapture = lidCaptureFor(senderJid, senderPhone);
|
|
4567
|
+
if (lidCapture) recordLidMapping(lidCapture.numericLid, lidCapture.e164);
|
|
4568
|
+
const selfPhone = conn.selfPhone ?? "";
|
|
4569
|
+
let accessResult;
|
|
4570
|
+
if (isGroup) {
|
|
4571
|
+
accessResult = checkGroupAccess({ groupJid: remoteJid });
|
|
4572
|
+
} else {
|
|
4573
|
+
accessResult = await resolveDmAccess({
|
|
4574
|
+
senderPhone,
|
|
4575
|
+
selfPhone,
|
|
4576
|
+
config: whatsAppConfig,
|
|
4577
|
+
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
4578
|
+
accountId: conn.platformAccountId,
|
|
4579
|
+
isValidAccount: (id) => isValidAccountId(id),
|
|
4580
|
+
resolvePersonByPhone
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
console.error(
|
|
4584
|
+
`${TAG16} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
|
|
4585
|
+
);
|
|
4586
|
+
if (!accessResult.allowed && accessResult.reason === "account-manager-unresolved") {
|
|
4587
|
+
console.error(
|
|
4588
|
+
`[whatsapp-native] op=account-manager-reject senderId=${senderPhone} reason=unresolved-effective-account`
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4591
|
+
if (isManagerRoutedToHouse({
|
|
4592
|
+
accessResult,
|
|
4593
|
+
houseAccountId: conn.houseAccountId
|
|
4594
|
+
})) {
|
|
4595
|
+
console.error(
|
|
4596
|
+
`[whatsapp-native] op=escalation-tripwire senderId=${senderPhone} reason=account-manager-routed-to-house effectiveAccount=${accessResult.effectiveAccountId}`
|
|
4597
|
+
);
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
if (!accessResult.allowed) return;
|
|
4601
|
+
if (isCustomerTurnSuppressed(accessResult.agentType, isHouseManagedService())) {
|
|
4602
|
+
console.error(
|
|
4603
|
+
`[channel-house] op=inbound from=${senderPhone} accountId=${conn.platformAccountId} autoReply=suppressed`
|
|
4604
|
+
);
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
4608
|
+
if (sendReceipts && msg.key.id) {
|
|
4609
|
+
sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0, conn.accountId);
|
|
4610
|
+
}
|
|
4611
|
+
let mediaResult;
|
|
4612
|
+
if (extracted.mediaType && conn.sock) {
|
|
4613
|
+
const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
|
|
4614
|
+
mediaResult = await downloadInboundMedia(msg, conn.sock, {
|
|
4615
|
+
maxBytes: maxMb * 1024 * 1024
|
|
4616
|
+
});
|
|
4617
|
+
if (!mediaResult) {
|
|
4618
|
+
console.error(`${TAG16} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
if (extracted.mediaType === "audio" && mediaResult?.path) {
|
|
4622
|
+
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4623
|
+
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4624
|
+
let resolvePending;
|
|
4625
|
+
const sttPending = new Promise((resolve6) => {
|
|
4626
|
+
resolvePending = resolve6;
|
|
4627
|
+
});
|
|
4628
|
+
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4629
|
+
try {
|
|
4630
|
+
const sttResult = await transcribe(mediaResult.path, mediaResult.mimetype);
|
|
4631
|
+
if (sttResult?.text) {
|
|
4632
|
+
extracted.text = sttResult.text;
|
|
4633
|
+
}
|
|
4634
|
+
} finally {
|
|
4635
|
+
resolvePending();
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
const reply = async (text) => {
|
|
4639
|
+
if (isGroupJid(remoteJid)) {
|
|
4640
|
+
console.error(`${TAG16} op=group-reply-blocked jid=${remoteJid}`);
|
|
4641
|
+
throw new WaReplyError("group reply blocked \u2014 observe-only", false);
|
|
4642
|
+
}
|
|
4643
|
+
const currentSock = conn.sock;
|
|
4644
|
+
if (!currentSock) {
|
|
4645
|
+
const reason = conn.terminalReason;
|
|
4646
|
+
throw new WaReplyError(reason ?? "WhatsApp disconnected \u2014 cannot reply", Boolean(reason));
|
|
4647
|
+
}
|
|
4648
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
4649
|
+
};
|
|
4650
|
+
const composing = async () => {
|
|
4651
|
+
const currentSock = conn.sock;
|
|
4652
|
+
if (currentSock) await sendComposing(currentSock, remoteJid);
|
|
4653
|
+
};
|
|
4654
|
+
const cacheKey = deriveCacheKey({
|
|
4655
|
+
agentType: accessResult.agentType,
|
|
4656
|
+
accountId: conn.accountId,
|
|
4657
|
+
senderPhone,
|
|
4658
|
+
isGroup,
|
|
4659
|
+
groupJid: isGroup ? remoteJid : void 0
|
|
4660
|
+
});
|
|
4661
|
+
conn.lastMessageAt = Date.now();
|
|
4662
|
+
const payload = {
|
|
4663
|
+
accountId: conn.accountId,
|
|
4664
|
+
agentType: accessResult.agentType,
|
|
4665
|
+
passive: accessResult.passive === true,
|
|
4666
|
+
personId: accessResult.personId ?? null,
|
|
4667
|
+
senderPhone,
|
|
4668
|
+
text: extracted.text,
|
|
4669
|
+
isGroup,
|
|
4670
|
+
groupJid: isGroup ? remoteJid : void 0,
|
|
4671
|
+
reply,
|
|
4672
|
+
composing,
|
|
4673
|
+
cacheKey,
|
|
4674
|
+
media: mediaResult?.path ? [{ path: mediaResult.path, type: extracted.mediaType, mimetype: mediaResult.mimetype }] : [],
|
|
4675
|
+
replyContext: extracted.quotedMessage,
|
|
4676
|
+
// Task 1383 — the gate's single resolution feeds the spawn (house or the
|
|
4677
|
+
// bound sub-account); the router never re-resolves.
|
|
4678
|
+
effectiveAccountId: accessResult.effectiveAccountId,
|
|
4679
|
+
// Task 1575 — the gate's owner-self verdict. Only reason:'self-phone' is the
|
|
4680
|
+
// owner's own number; adminPhones/account-manager admits are other admins
|
|
4681
|
+
// and must not inherit the owner userId.
|
|
4682
|
+
ownerSelfPhone: accessResult.reason === "self-phone"
|
|
4683
|
+
};
|
|
4684
|
+
if (accessResult.agentType === "public") {
|
|
4685
|
+
const hoursResult = await isBusinessOpen(conn.accountId);
|
|
4686
|
+
if (!hoursResult.open) {
|
|
4687
|
+
console.error(`${TAG16} [${conn.accountId}] dispatch skipped: business closed`);
|
|
4688
|
+
const afterHoursMessage = whatsAppConfig.accounts?.[conn.accountId]?.afterHoursMessage ?? whatsAppConfig.afterHoursMessage;
|
|
4689
|
+
if (afterHoursMessage) {
|
|
4690
|
+
try {
|
|
4691
|
+
await reply(afterHoursMessage);
|
|
4692
|
+
console.error(`${TAG16} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
|
|
4693
|
+
} catch (err) {
|
|
4694
|
+
console.error(
|
|
4695
|
+
`${TAG16} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
if (conn.debouncer) {
|
|
4703
|
+
conn.debouncer.enqueue(payload);
|
|
4704
|
+
} else {
|
|
4705
|
+
dispatchInboundPayload(conn, payload);
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
export {
|
|
4710
|
+
STORE_ROOT,
|
|
4711
|
+
listStoredAccountIds,
|
|
4712
|
+
conversationFilePath,
|
|
4713
|
+
appendMessage,
|
|
4714
|
+
readConversation,
|
|
4715
|
+
readReactions,
|
|
4716
|
+
foldReactions,
|
|
4717
|
+
readConversationSummaries,
|
|
4718
|
+
readAllConversations,
|
|
4719
|
+
enumerateValidAccountIds,
|
|
4720
|
+
resolveHouseOrSoleAccountId,
|
|
4721
|
+
getAccountsDirFromEnv,
|
|
4722
|
+
validateAccountIdEnv,
|
|
4723
|
+
containedSinceBoot,
|
|
4724
|
+
mediaDownloadsInFlight,
|
|
4725
|
+
guardInstalled,
|
|
4726
|
+
installMediaDownloadGuard,
|
|
4727
|
+
readDeclined,
|
|
4728
|
+
appendNotifyPublished,
|
|
4729
|
+
readNotifyRows,
|
|
4730
|
+
readLedger,
|
|
4731
|
+
pruneLedger,
|
|
4732
|
+
listLedgerAccounts,
|
|
4733
|
+
decideWaNotify,
|
|
4734
|
+
WhatsAppAccountSchema,
|
|
4735
|
+
WhatsAppConfigSchema,
|
|
4736
|
+
parseWhatsAppConfigTolerant,
|
|
4737
|
+
hasCredsSync,
|
|
4738
|
+
authExists,
|
|
4739
|
+
readSelfId,
|
|
4740
|
+
credsCensus,
|
|
4741
|
+
getAuthAgeMs,
|
|
4742
|
+
clearAuth,
|
|
4743
|
+
discardAuthDirSync,
|
|
4744
|
+
drainCredsSaveQueue,
|
|
4745
|
+
createWaSocket,
|
|
4746
|
+
waitForConnection,
|
|
4747
|
+
getStatusCode,
|
|
4748
|
+
formatError,
|
|
4749
|
+
closeKind,
|
|
4750
|
+
classifyDisconnect,
|
|
4751
|
+
normalizeE164,
|
|
4752
|
+
toWhatsappJid,
|
|
4753
|
+
jidToE164,
|
|
4754
|
+
senderJidFromTelephone,
|
|
4755
|
+
isGroupJid,
|
|
4756
|
+
isUserJid,
|
|
4757
|
+
normalizeTarget,
|
|
4758
|
+
phonesMatch,
|
|
4759
|
+
resolveViaLidMap,
|
|
4760
|
+
readLidMap,
|
|
4761
|
+
managedAccountFor,
|
|
4762
|
+
managerPhonesForAccount,
|
|
4763
|
+
isAdminPhone,
|
|
4764
|
+
shouldDispatchInbound,
|
|
4765
|
+
checkAccessRateLimit,
|
|
4766
|
+
recordAccessFailedAttempt,
|
|
4767
|
+
clearAccessRateLimit,
|
|
4768
|
+
checkRequestLinkRateLimit,
|
|
4769
|
+
recordRequestLinkAttempt,
|
|
4770
|
+
maskContact,
|
|
4771
|
+
findGrantByMagicToken,
|
|
4772
|
+
findActiveGrantByContact,
|
|
4773
|
+
findGrantByContact,
|
|
4774
|
+
ensureManagerPerson,
|
|
4775
|
+
enrolPerson,
|
|
4776
|
+
consumeMagicTokenAndActivate,
|
|
4777
|
+
generateNewMagicToken,
|
|
4778
|
+
getChannelActivity,
|
|
4779
|
+
sendTextMessage,
|
|
4780
|
+
sendComposing,
|
|
4781
|
+
sendPaused,
|
|
4782
|
+
sendMediaMessage,
|
|
4783
|
+
resolvePlatformAccountId,
|
|
4784
|
+
warnNonUuidAccount,
|
|
4785
|
+
isHouseManagedService,
|
|
4786
|
+
isCustomerTurnSuppressed,
|
|
4787
|
+
DATA_ROOT,
|
|
4788
|
+
crossesForeignAccountPartition,
|
|
4789
|
+
isWithinOwnAccountPartition,
|
|
4790
|
+
resolveDataPath,
|
|
4791
|
+
resolveClaudeUploadsPath,
|
|
4792
|
+
sessionScratchpadRoot,
|
|
4793
|
+
resolveScratchpadPath,
|
|
4794
|
+
SUPPORTED_MIME_TYPES,
|
|
4795
|
+
MAX_FILE_SIZE_BYTES,
|
|
4796
|
+
MAX_FILES_PER_MESSAGE,
|
|
4797
|
+
MAX_DATA_UPLOAD_BYTES,
|
|
4798
|
+
EXTRACTED_TEXT_SUFFIX,
|
|
4799
|
+
assertSupportedMime,
|
|
4800
|
+
isInlineSafeMime,
|
|
4801
|
+
resolveStoredAttachment,
|
|
4802
|
+
storeAttachment,
|
|
4803
|
+
storePublicAttachment,
|
|
4804
|
+
detectMimeType,
|
|
4805
|
+
validateFilePathInAccount,
|
|
4806
|
+
storeComponentArtefact,
|
|
4807
|
+
WHATSAPP_DOCUMENT_MAX_BYTES,
|
|
4808
|
+
transcribe,
|
|
4809
|
+
WaReplyError,
|
|
4810
|
+
buildNotifyPending,
|
|
4811
|
+
isObserveAccount,
|
|
4812
|
+
getWhatsAppConfig,
|
|
4813
|
+
messageStoreKey,
|
|
4814
|
+
storeMessage,
|
|
4815
|
+
deriveCacheKey,
|
|
4816
|
+
buildSelfChatDispatch,
|
|
4817
|
+
init,
|
|
4818
|
+
startConnection,
|
|
4819
|
+
stopConnection,
|
|
4820
|
+
getStatus,
|
|
4821
|
+
getSocket,
|
|
4822
|
+
selectConnection,
|
|
4823
|
+
resolveSocket,
|
|
4824
|
+
selectStoreAccountKey,
|
|
4825
|
+
resolveReadStoreKey,
|
|
4826
|
+
resolveInboundPersistAccount,
|
|
4827
|
+
isManagerRoutedToHouse,
|
|
4828
|
+
resolveConversationPersistAccount,
|
|
4829
|
+
isAnyConnected,
|
|
4830
|
+
listConnectionPlatformAccountIds,
|
|
4831
|
+
registerLoginSocket,
|
|
4832
|
+
reloadConfig,
|
|
4833
|
+
getMessages,
|
|
4834
|
+
listConversationJids,
|
|
4835
|
+
listStoreAccountKeys,
|
|
4836
|
+
shutdown
|
|
4837
|
+
};
|