crewly 1.20.122 → 1.20.123

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.
Files changed (75) hide show
  1. package/config/skills/agent/core/whatsapp-draft/SKILL.md +80 -0
  2. package/config/skills/agent/core/whatsapp-draft/execute.sh +84 -0
  3. package/config/skills/agent/core/whatsapp-draft/execute.test.sh +70 -0
  4. package/config/skills/agent/core/whatsapp-inbox/SKILL.md +75 -0
  5. package/config/skills/agent/core/whatsapp-inbox/execute.sh +87 -0
  6. package/config/skills/agent/core/whatsapp-inbox/execute.test.sh +68 -0
  7. package/config/skills/agent/core/whatsapp-read/SKILL.md +80 -0
  8. package/config/skills/agent/core/whatsapp-read/execute.sh +116 -0
  9. package/config/skills/agent/core/whatsapp-read/execute.test.sh +84 -0
  10. package/config/skills/agent/core/whatsapp-send/SKILL.md +70 -0
  11. package/config/skills/agent/core/whatsapp-send/execute.sh +84 -0
  12. package/config/skills/agent/core/whatsapp-send/execute.test.sh +79 -0
  13. package/dist/backend/backend/src/constants.d.ts +107 -0
  14. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  15. package/dist/backend/backend/src/constants.js +105 -0
  16. package/dist/backend/backend/src/constants.js.map +1 -1
  17. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts +73 -0
  18. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts.map +1 -0
  19. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js +310 -0
  20. package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js.map +1 -0
  21. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +53 -4
  23. package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -1
  24. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  25. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  26. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  27. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +29 -1
  29. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  30. package/dist/backend/backend/src/services/whatsapp/index.d.ts +5 -1
  31. package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -1
  32. package/dist/backend/backend/src/services/whatsapp/index.js +5 -1
  33. package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -1
  34. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts +59 -0
  35. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts.map +1 -0
  36. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js +108 -0
  37. package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js.map +1 -0
  38. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts +74 -0
  39. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts.map +1 -0
  40. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js +82 -0
  41. package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js.map +1 -0
  42. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts +184 -0
  43. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts.map +1 -0
  44. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js +420 -0
  45. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js.map +1 -0
  46. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts +243 -0
  47. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts.map +1 -0
  48. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js +538 -0
  49. package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js.map +1 -0
  50. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +32 -2
  51. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +65 -9
  53. package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -1
  54. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +11 -0
  56. package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
  57. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +59 -1
  58. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +159 -9
  60. package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -1
  61. package/dist/backend/backend/src/types/whatsapp.types.d.ts +108 -0
  62. package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -1
  63. package/dist/backend/backend/src/types/whatsapp.types.js +9 -0
  64. package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -1
  65. package/dist/backend/build-info.json +2 -2
  66. package/dist/cli/backend/src/constants.d.ts +107 -0
  67. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  68. package/dist/cli/backend/src/constants.js +105 -0
  69. package/dist/cli/backend/src/constants.js.map +1 -1
  70. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  71. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
  72. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  73. package/frontend/dist/assets/{index-07e338a9.js → index-daf43f1a.js} +268 -268
  74. package/frontend/dist/index.html +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WhatsApp Draft Send Gate
3
+ *
4
+ * Decides whether a reply draft may be sent. Owner decision (2026-09-24):
5
+ * nothing is ever sent to WhatsApp without the owner's explicit
6
+ * confirmation, one message at a time.
7
+ *
8
+ * - A request with no `X-Agent-Session` header is the owner (dashboard,
9
+ * mobile, portal) — the click is the confirmation.
10
+ * - An agent may send only when a genuine owner chat message (from
11
+ * `ChatV2Service.getRecentOwnerMessageContents`, which agents cannot
12
+ * write) created after the draft says 「发 W12」 for this draft's code, and
13
+ * the draft is at most `DRAFT_CONFIRM_WINDOW_MS` old. Because the owner
14
+ * message must post-date the draft, bounding the draft's age also bounds
15
+ * the confirmation to that window.
16
+ *
17
+ * @module services/whatsapp/whatsapp-draft-gate
18
+ */
19
+ import { WHATSAPP_CONSTANTS } from '../../constants.js';
20
+ import { parseDraftCode } from './whatsapp-inbox.store.js';
21
+ /** Milliseconds per minute (for human-readable window text). */
22
+ const MS_PER_MINUTE = 60 * 1000;
23
+ /**
24
+ * Whether one owner message confirms sending a given draft.
25
+ *
26
+ * @param text - Owner message text
27
+ * @param draft - The draft
28
+ * @returns True for 「发 W12」 / 「发送 12」 / "send #W12" / 「确认发送 W12」 naming this draft
29
+ */
30
+ export function isConfirmationFor(text, draft) {
31
+ const m = WHATSAPP_CONSTANTS.DRAFT_CONFIRM_PATTERN.exec(text.trim());
32
+ if (!m)
33
+ return false;
34
+ return parseDraftCode(m[2]) === draft.seq;
35
+ }
36
+ /**
37
+ * Decide whether the caller may send this draft now.
38
+ *
39
+ * @param ctx - Draft, caller, owner-message source, clock
40
+ * @returns Allowed (and why), or refused (and why)
41
+ */
42
+ export function decideDraftSend(ctx) {
43
+ if (!ctx.agentSession)
44
+ return { allowed: true, via: 'owner' };
45
+ if (ctx.now - ctx.draft.createdAt > WHATSAPP_CONSTANTS.DRAFT_CONFIRM_WINDOW_MS) {
46
+ return { allowed: false, reason: 'window_expired' };
47
+ }
48
+ const confirmation = ctx.ownerMessagesSince(ctx.draft.createdAt).find((t) => isConfirmationFor(t, ctx.draft));
49
+ return confirmation !== undefined
50
+ ? { allowed: true, via: 'owner_confirmation', confirmation }
51
+ : { allowed: false, reason: 'no_confirmation' };
52
+ }
53
+ /**
54
+ * The instruction an agent must follow after drafting: show the owner the
55
+ * draft and ask for 「发 <code>」.
56
+ *
57
+ * @param draft - The draft
58
+ * @param recipient - Human label for the recipient (chat name or JID)
59
+ * @returns Instruction text
60
+ */
61
+ export function buildOwnerConfirmPrompt(draft, recipient) {
62
+ return (`Draft ${draft.code} is saved but NOT sent. Show the owner the recipient (${recipient}), ` +
63
+ `the exact draft text, and the code ${draft.code}, and ask them to reply 「发 ${draft.code}」 to send it ` +
64
+ `(or send/discard it from the dashboard). Do not send it until they do.`);
65
+ }
66
+ /**
67
+ * Human message for a refused agent send.
68
+ *
69
+ * @param draft - The draft
70
+ * @param reason - Refusal reason
71
+ * @returns Message for the 403 body
72
+ */
73
+ export function buildRefusalMessage(draft, reason) {
74
+ const windowMinutes = Math.round(WHATSAPP_CONSTANTS.DRAFT_CONFIRM_WINDOW_MS / MS_PER_MINUTE);
75
+ if (reason === 'window_expired') {
76
+ return (`Draft ${draft.code} is older than ${windowMinutes} minutes, so it can no longer be sent on the owner's ` +
77
+ `chat confirmation. Ask the owner to send it from the dashboard, or write a fresh draft and ask them to reply 「发 <new code>」.`);
78
+ }
79
+ return (`The owner has not confirmed draft ${draft.code}. Show the owner the draft (recipient and exact text) ` +
80
+ `and ask them to reply 「发 ${draft.code}」. Only then call send again. Never send without it.`);
81
+ }
82
+ //# sourceMappingURL=whatsapp-draft-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-draft-gate.js","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-draft-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE3D,gEAAgE;AAChE,MAAM,aAAa,GAAG,EAAE,GAAG,IAAI,CAAC;AAsBhC;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAiC;IAC/E,MAAM,CAAC,GAAG,kBAAkB,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACrE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAqB;IACnD,IAAI,CAAC,GAAG,CAAC,YAAY;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAC9D,IAAI,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,kBAAkB,CAAC,uBAAuB,EAAE,CAAC;QAC/E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IACtD,CAAC;IACD,MAAM,YAAY,GAAG,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9G,OAAO,YAAY,KAAK,SAAS;QAC/B,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,oBAAoB,EAAE,YAAY,EAAE;QAC5D,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAA2C,EAAE,SAAiB;IACpG,OAAO,CACL,SAAS,KAAK,CAAC,IAAI,yDAAyD,SAAS,KAAK;QAC1F,sCAAsC,KAAK,CAAC,IAAI,8BAA8B,KAAK,CAAC,IAAI,eAAe;QACvG,wEAAwE,CACzE,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAkC,EAAE,MAAwB;IAC9F,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,uBAAuB,GAAG,aAAa,CAAC,CAAC;IAC7F,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,OAAO,CACL,SAAS,KAAK,CAAC,IAAI,kBAAkB,aAAa,uDAAuD;YACzG,8HAA8H,CAC/H,CAAC;IACJ,CAAC;IACD,OAAO,CACL,qCAAqC,KAAK,CAAC,IAAI,wDAAwD;QACvG,4BAA4B,KAAK,CAAC,IAAI,sDAAsD,CAC7F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * WhatsApp Inbox Capture
3
+ *
4
+ * Turns Baileys events into inbox-store rows. Used only in `inbox` mode.
5
+ * Unlike the assistant path (`WhatsAppService.handleIncomingMessage`) nothing
6
+ * is dropped for being the owner's own message, a group message, media, or
7
+ * from a contact outside `allowedContacts` — the inbox has to know whether
8
+ * the owner already replied, and it is the owner's own account.
9
+ *
10
+ * Media are never downloaded: a message is recorded as its kind plus caption
11
+ * (or file name for documents).
12
+ *
13
+ * Baileys shapes relied on (v7, `@whiskeysockets/baileys` typings):
14
+ * - `messages.upsert`: `{ messages: WAMessage[], type: 'notify' | 'append' }`
15
+ * - `messaging-history.set`: `{ chats: Chat[], contacts: Contact[], messages: WAMessage[], syncType? }`
16
+ * - `contacts.upsert` / `contacts.update`: `Contact[]` (`id`, `name` = address book, `notify` = their pushName)
17
+ * - `chats.upsert` / `chats.update`: `Chat[]` (`id`, `name`, `conversationTimestamp`)
18
+ * - `groups.upsert` / `groups.update`: `GroupMetadata[]` (`id`, `subject`)
19
+ * - `WAMessage.key.remoteJidAlt` / `participantAlt` (v7 LID↔PN alternates)
20
+ * - `messageTimestamp`: seconds, as number, numeric string or protobuf Long
21
+ *
22
+ * @module services/whatsapp/whatsapp-inbox-capture
23
+ */
24
+ import type { WhatsAppInboxMessage, WhatsAppMessageKind } from '../../types/whatsapp.types.js';
25
+ import type { UpsertChatInput, WhatsAppInboxStore } from './whatsapp-inbox.store.js';
26
+ /** Loose record type for untyped Baileys payloads. */
27
+ type Rec = Record<string, unknown>;
28
+ /**
29
+ * Convert a Baileys timestamp (seconds as number, numeric string, or
30
+ * protobuf Long with `toNumber()` / `low`+`high`) to epoch ms.
31
+ *
32
+ * @param ts - Raw timestamp
33
+ * @returns Epoch ms, or null when unparseable / non-positive
34
+ */
35
+ export declare function toEpochMs(ts: unknown): number | null;
36
+ /**
37
+ * Whether a JID is a group chat.
38
+ *
39
+ * @param jid - JID
40
+ * @returns True for `...@g.us`
41
+ */
42
+ export declare function isGroupJid(jid: string): boolean;
43
+ /**
44
+ * Pick the canonical JID for a person: prefer the phone-number form when
45
+ * Baileys supplies it as the alternate of a LID, so the same person's
46
+ * messages (some keyed by LID, some by phone number) land in one chat.
47
+ *
48
+ * @param jid - Primary JID (`remoteJid` / `participant`)
49
+ * @param alt - Alternate JID (`remoteJidAlt` / `participantAlt`)
50
+ * @returns The canonical JID
51
+ */
52
+ export declare function canonicalJid(jid: string, alt?: string): string;
53
+ /**
54
+ * Strip wrapper messages (disappearing, view-once, edited, doc-with-caption).
55
+ *
56
+ * @param content - `WAMessage.message`
57
+ * @returns The innermost content object, or undefined
58
+ */
59
+ export declare function unwrapMessageContent(content: unknown): Rec | undefined;
60
+ /**
61
+ * Classify message content into a stored kind and its text/caption.
62
+ *
63
+ * @param content - `WAMessage.message` (wrappers allowed)
64
+ * @returns `{ kind, text }`, or null when this is not a user-visible message
65
+ * (protocol message, reaction, empty envelope)
66
+ */
67
+ export declare function classifyMessageContent(content: unknown): {
68
+ kind: WhatsAppMessageKind;
69
+ text: string;
70
+ } | null;
71
+ /** Result of parsing one Baileys message. */
72
+ export interface ParsedWhatsAppMessage {
73
+ /** Row for the store */
74
+ message: WhatsAppInboxMessage;
75
+ /** Whether the chat is a group */
76
+ isGroup: boolean;
77
+ /** Sender's self-chosen name, when the message carried one (not for own messages) */
78
+ pushName: string | null;
79
+ }
80
+ /**
81
+ * Parse a Baileys `WAMessage` into an inbox row.
82
+ *
83
+ * @param raw - The WAMessage
84
+ * @param ownJid - The connected account's JID (sender of `fromMe` messages), if known
85
+ * @returns The parsed message, or null when it is not storable (no id/chat,
86
+ * status broadcast, protocol-only content, no timestamp)
87
+ */
88
+ export declare function parseBaileysMessage(raw: unknown, ownJid?: string | null): ParsedWhatsAppMessage | null;
89
+ /**
90
+ * Chat row from a Baileys `Contact`: the address-book name outranks the
91
+ * contact's own pushName (`notify`) and business `verifiedName`.
92
+ *
93
+ * @param contact - Contact (full or partial update)
94
+ * @returns Upsert input, or null when the contact names nothing useful
95
+ */
96
+ export declare function chatFromContact(contact: unknown): UpsertChatInput | null;
97
+ /**
98
+ * Chat row from a Baileys `Chat` (history sync, `chats.upsert/update`).
99
+ *
100
+ * @param chat - Chat
101
+ * @returns Upsert input, or null without an id / for the status pseudo-chat
102
+ */
103
+ export declare function chatFromChat(chat: unknown): UpsertChatInput | null;
104
+ /**
105
+ * Chat row from Baileys `GroupMetadata` (subject = group name).
106
+ *
107
+ * @param group - Group metadata (full or partial)
108
+ * @returns Upsert input, or null without an id
109
+ */
110
+ export declare function chatFromGroup(group: unknown): UpsertChatInput | null;
111
+ /** Optional hooks for {@link WhatsAppInboxCapture}. */
112
+ export interface WhatsAppInboxCaptureOptions {
113
+ /** The connected account's JID (sender of own messages) */
114
+ getOwnJid?: () => string | null;
115
+ /** Fetch a group's subject for a group we have no name for (best effort) */
116
+ fetchGroupSubject?: (jid: string) => Promise<string | null>;
117
+ /** Clock (tests) */
118
+ now?: () => number;
119
+ }
120
+ /**
121
+ * Writes Baileys events into a {@link WhatsAppInboxStore}.
122
+ *
123
+ * Every handler is defensive: a malformed payload is skipped, never thrown,
124
+ * because it runs inside the socket's event loop.
125
+ */
126
+ export declare class WhatsAppInboxCapture {
127
+ private readonly store;
128
+ private readonly opts;
129
+ private readonly groupLookups;
130
+ /**
131
+ * @param store - Destination store
132
+ * @param opts - Own-JID accessor, group-subject fetcher, clock
133
+ */
134
+ constructor(store: WhatsAppInboxStore, opts?: WhatsAppInboxCaptureOptions);
135
+ /**
136
+ * Store one message and name its chat.
137
+ *
138
+ * @param raw - A WAMessage
139
+ * @returns True when stored
140
+ */
141
+ captureMessage(raw: unknown): boolean;
142
+ /**
143
+ * Handle `messages.upsert` (both `notify` and `append`: own messages sent
144
+ * from the phone and backfills arrive as `append`).
145
+ *
146
+ * @param upsert - `{ messages, type }`
147
+ * @returns Number of messages stored
148
+ */
149
+ onMessagesUpsert(upsert: unknown): number;
150
+ /**
151
+ * Handle `messaging-history.set`: seed chats, contact names and recent
152
+ * messages. Messages older than `HISTORY_SYNC_MAX_AGE_MS` are skipped.
153
+ *
154
+ * @param payload - `{ chats, contacts, messages }`
155
+ * @returns Number of messages stored
156
+ */
157
+ onHistorySet(payload: unknown): number;
158
+ /**
159
+ * Handle `contacts.upsert` / `contacts.update`.
160
+ *
161
+ * @param contacts - Contact list
162
+ */
163
+ onContacts(contacts: unknown): void;
164
+ /**
165
+ * Handle `chats.upsert` / `chats.update`.
166
+ *
167
+ * @param chats - Chat list
168
+ */
169
+ onChats(chats: unknown): void;
170
+ /**
171
+ * Handle `groups.upsert` / `groups.update`.
172
+ *
173
+ * @param groups - Group metadata list
174
+ */
175
+ onGroups(groups: unknown): void;
176
+ /**
177
+ * Look a group's subject up once per process when we have no name for it.
178
+ *
179
+ * @param jid - Group JID
180
+ */
181
+ private maybeFetchGroupName;
182
+ }
183
+ export {};
184
+ //# sourceMappingURL=whatsapp-inbox-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-inbox-capture.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/whatsapp/whatsapp-inbox-capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,420 @@
1
+ /**
2
+ * WhatsApp Inbox Capture
3
+ *
4
+ * Turns Baileys events into inbox-store rows. Used only in `inbox` mode.
5
+ * Unlike the assistant path (`WhatsAppService.handleIncomingMessage`) nothing
6
+ * is dropped for being the owner's own message, a group message, media, or
7
+ * from a contact outside `allowedContacts` — the inbox has to know whether
8
+ * the owner already replied, and it is the owner's own account.
9
+ *
10
+ * Media are never downloaded: a message is recorded as its kind plus caption
11
+ * (or file name for documents).
12
+ *
13
+ * Baileys shapes relied on (v7, `@whiskeysockets/baileys` typings):
14
+ * - `messages.upsert`: `{ messages: WAMessage[], type: 'notify' | 'append' }`
15
+ * - `messaging-history.set`: `{ chats: Chat[], contacts: Contact[], messages: WAMessage[], syncType? }`
16
+ * - `contacts.upsert` / `contacts.update`: `Contact[]` (`id`, `name` = address book, `notify` = their pushName)
17
+ * - `chats.upsert` / `chats.update`: `Chat[]` (`id`, `name`, `conversationTimestamp`)
18
+ * - `groups.upsert` / `groups.update`: `GroupMetadata[]` (`id`, `subject`)
19
+ * - `WAMessage.key.remoteJidAlt` / `participantAlt` (v7 LID↔PN alternates)
20
+ * - `messageTimestamp`: seconds, as number, numeric string or protobuf Long
21
+ *
22
+ * @module services/whatsapp/whatsapp-inbox-capture
23
+ */
24
+ import { WHATSAPP_CONSTANTS } from '../../constants.js';
25
+ /** Milliseconds per second (Baileys timestamps are seconds). */
26
+ const MS_PER_SECOND = 1000;
27
+ /** Max wrapper depth unwrapped (ephemeral → viewOnce → ...), as Baileys does. */
28
+ const MAX_UNWRAP_DEPTH = 5;
29
+ /** Wrapper message keys whose `.message` holds the real content. */
30
+ const WRAPPER_KEYS = [
31
+ 'ephemeralMessage',
32
+ 'viewOnceMessage',
33
+ 'viewOnceMessageV2',
34
+ 'viewOnceMessageV2Extension',
35
+ 'documentWithCaptionMessage',
36
+ 'editedMessage',
37
+ ];
38
+ /**
39
+ * Content keys that are not user-visible messages (receipts of protocol
40
+ * work, reactions, key distribution). A message carrying only these is not
41
+ * stored.
42
+ */
43
+ const NON_MESSAGE_KEYS = new Set([
44
+ 'protocolMessage',
45
+ 'reactionMessage',
46
+ 'senderKeyDistributionMessage',
47
+ 'messageContextInfo',
48
+ 'pollUpdateMessage',
49
+ 'keepInChatMessage',
50
+ 'encReactionMessage',
51
+ ]);
52
+ /**
53
+ * Narrow an unknown to a plain object.
54
+ *
55
+ * @param v - Anything
56
+ * @returns The object, or undefined
57
+ */
58
+ function asRec(v) {
59
+ return v && typeof v === 'object' ? v : undefined;
60
+ }
61
+ /**
62
+ * Read a non-empty string field.
63
+ *
64
+ * @param v - Anything
65
+ * @returns The string, or undefined when absent/blank
66
+ */
67
+ function str(v) {
68
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
69
+ }
70
+ /**
71
+ * Convert a Baileys timestamp (seconds as number, numeric string, or
72
+ * protobuf Long with `toNumber()` / `low`+`high`) to epoch ms.
73
+ *
74
+ * @param ts - Raw timestamp
75
+ * @returns Epoch ms, or null when unparseable / non-positive
76
+ */
77
+ export function toEpochMs(ts) {
78
+ let seconds = null;
79
+ if (typeof ts === 'number')
80
+ seconds = ts;
81
+ else if (typeof ts === 'bigint')
82
+ seconds = Number(ts);
83
+ else if (typeof ts === 'string' && /^\d+$/.test(ts))
84
+ seconds = Number(ts);
85
+ else {
86
+ const r = asRec(ts);
87
+ if (r && typeof r.toNumber === 'function')
88
+ seconds = r.toNumber();
89
+ else if (r && typeof r.low === 'number') {
90
+ const high = typeof r.high === 'number' ? r.high : 0;
91
+ seconds = high * 2 ** 32 + (r.low >>> 0);
92
+ }
93
+ }
94
+ if (seconds === null || !Number.isFinite(seconds) || seconds <= 0)
95
+ return null;
96
+ return Math.round(seconds * MS_PER_SECOND);
97
+ }
98
+ /**
99
+ * Whether a JID is a group chat.
100
+ *
101
+ * @param jid - JID
102
+ * @returns True for `...@g.us`
103
+ */
104
+ export function isGroupJid(jid) {
105
+ return jid.endsWith(WHATSAPP_CONSTANTS.GROUP_JID_SUFFIX);
106
+ }
107
+ /**
108
+ * Pick the canonical JID for a person: prefer the phone-number form when
109
+ * Baileys supplies it as the alternate of a LID, so the same person's
110
+ * messages (some keyed by LID, some by phone number) land in one chat.
111
+ *
112
+ * @param jid - Primary JID (`remoteJid` / `participant`)
113
+ * @param alt - Alternate JID (`remoteJidAlt` / `participantAlt`)
114
+ * @returns The canonical JID
115
+ */
116
+ export function canonicalJid(jid, alt) {
117
+ if (jid.endsWith(WHATSAPP_CONSTANTS.LID_JID_SUFFIX) && alt && alt.endsWith(WHATSAPP_CONSTANTS.USER_JID_SUFFIX)) {
118
+ return alt;
119
+ }
120
+ return jid;
121
+ }
122
+ /**
123
+ * Strip wrapper messages (disappearing, view-once, edited, doc-with-caption).
124
+ *
125
+ * @param content - `WAMessage.message`
126
+ * @returns The innermost content object, or undefined
127
+ */
128
+ export function unwrapMessageContent(content) {
129
+ let cur = asRec(content);
130
+ for (let i = 0; i < MAX_UNWRAP_DEPTH && cur; i++) {
131
+ let inner;
132
+ for (const k of WRAPPER_KEYS) {
133
+ inner = asRec(asRec(cur[k])?.message);
134
+ if (inner)
135
+ break;
136
+ }
137
+ if (!inner)
138
+ break;
139
+ cur = inner;
140
+ }
141
+ return cur;
142
+ }
143
+ /**
144
+ * Classify message content into a stored kind and its text/caption.
145
+ *
146
+ * @param content - `WAMessage.message` (wrappers allowed)
147
+ * @returns `{ kind, text }`, or null when this is not a user-visible message
148
+ * (protocol message, reaction, empty envelope)
149
+ */
150
+ export function classifyMessageContent(content) {
151
+ const c = unwrapMessageContent(content);
152
+ if (!c)
153
+ return null;
154
+ const K = WHATSAPP_CONSTANTS.MESSAGE_KINDS;
155
+ const conversation = str(c.conversation);
156
+ if (conversation)
157
+ return { kind: K.TEXT, text: conversation };
158
+ const ext = asRec(c.extendedTextMessage);
159
+ if (ext)
160
+ return { kind: K.TEXT, text: str(ext.text) ?? '' };
161
+ const image = asRec(c.imageMessage);
162
+ if (image)
163
+ return { kind: K.IMAGE, text: str(image.caption) ?? '' };
164
+ const video = asRec(c.videoMessage) ?? asRec(c.ptvMessage);
165
+ if (video)
166
+ return { kind: K.VIDEO, text: str(video.caption) ?? '' };
167
+ const doc = asRec(c.documentMessage);
168
+ if (doc)
169
+ return { kind: K.DOCUMENT, text: str(doc.caption) ?? str(doc.fileName) ?? str(doc.title) ?? '' };
170
+ if (asRec(c.audioMessage))
171
+ return { kind: K.AUDIO, text: '' };
172
+ if (asRec(c.stickerMessage))
173
+ return { kind: K.STICKER, text: '' };
174
+ const visibleKeys = Object.keys(c).filter((k) => c[k] !== null && c[k] !== undefined && !NON_MESSAGE_KEYS.has(k));
175
+ if (visibleKeys.length === 0)
176
+ return null;
177
+ return { kind: K.OTHER, text: '' };
178
+ }
179
+ /**
180
+ * Parse a Baileys `WAMessage` into an inbox row.
181
+ *
182
+ * @param raw - The WAMessage
183
+ * @param ownJid - The connected account's JID (sender of `fromMe` messages), if known
184
+ * @returns The parsed message, or null when it is not storable (no id/chat,
185
+ * status broadcast, protocol-only content, no timestamp)
186
+ */
187
+ export function parseBaileysMessage(raw, ownJid) {
188
+ const r = asRec(raw);
189
+ const key = asRec(r?.key);
190
+ const remoteJid = str(key?.remoteJid);
191
+ const id = str(key?.id);
192
+ if (!r || !key || !remoteJid || !id)
193
+ return null;
194
+ if (remoteJid === WHATSAPP_CONSTANTS.STATUS_BROADCAST_JID)
195
+ return null;
196
+ const classified = classifyMessageContent(r.message);
197
+ if (!classified)
198
+ return null;
199
+ const ts = toEpochMs(r.messageTimestamp);
200
+ if (ts === null)
201
+ return null;
202
+ const fromMe = key.fromMe === true;
203
+ const chatId = canonicalJid(remoteJid, str(key.remoteJidAlt));
204
+ const isGroup = isGroupJid(chatId);
205
+ const participant = str(key.participant);
206
+ const pushName = fromMe ? null : (str(r.pushName) ?? null);
207
+ let senderJid;
208
+ if (fromMe)
209
+ senderJid = ownJid ?? null;
210
+ else if (isGroup)
211
+ senderJid = participant ? canonicalJid(participant, str(key.participantAlt)) : null;
212
+ else
213
+ senderJid = chatId;
214
+ return {
215
+ message: {
216
+ id,
217
+ chatId,
218
+ fromMe,
219
+ senderJid,
220
+ senderName: pushName,
221
+ text: classified.text,
222
+ ts,
223
+ kind: classified.kind,
224
+ },
225
+ isGroup,
226
+ pushName,
227
+ };
228
+ }
229
+ /**
230
+ * Chat row from a Baileys `Contact`: the address-book name outranks the
231
+ * contact's own pushName (`notify`) and business `verifiedName`.
232
+ *
233
+ * @param contact - Contact (full or partial update)
234
+ * @returns Upsert input, or null when the contact names nothing useful
235
+ */
236
+ export function chatFromContact(contact) {
237
+ const c = asRec(contact);
238
+ const rawId = str(c?.id);
239
+ if (!c || !rawId)
240
+ return null;
241
+ const id = canonicalJid(rawId, str(c.phoneNumber));
242
+ const R = WHATSAPP_CONSTANTS.NAME_RANKS;
243
+ const saved = str(c.name);
244
+ if (saved)
245
+ return { id, name: saved, nameRank: R.CONTACT };
246
+ const other = str(c.notify) ?? str(c.verifiedName);
247
+ if (other)
248
+ return { id, name: other, nameRank: R.PUSH_NAME };
249
+ return null;
250
+ }
251
+ /**
252
+ * Chat row from a Baileys `Chat` (history sync, `chats.upsert/update`).
253
+ *
254
+ * @param chat - Chat
255
+ * @returns Upsert input, or null without an id / for the status pseudo-chat
256
+ */
257
+ export function chatFromChat(chat) {
258
+ const c = asRec(chat);
259
+ const id = str(c?.id);
260
+ if (!c || !id || id === WHATSAPP_CONSTANTS.STATUS_BROADCAST_JID)
261
+ return null;
262
+ return {
263
+ id,
264
+ name: str(c.name) ?? null,
265
+ nameRank: WHATSAPP_CONSTANTS.NAME_RANKS.CHAT,
266
+ isGroup: isGroupJid(id),
267
+ lastMessageAt: toEpochMs(c.conversationTimestamp),
268
+ };
269
+ }
270
+ /**
271
+ * Chat row from Baileys `GroupMetadata` (subject = group name).
272
+ *
273
+ * @param group - Group metadata (full or partial)
274
+ * @returns Upsert input, or null without an id
275
+ */
276
+ export function chatFromGroup(group) {
277
+ const g = asRec(group);
278
+ const id = str(g?.id);
279
+ if (!g || !id)
280
+ return null;
281
+ return { id, name: str(g.subject) ?? null, nameRank: WHATSAPP_CONSTANTS.NAME_RANKS.CHAT, isGroup: true };
282
+ }
283
+ /**
284
+ * Writes Baileys events into a {@link WhatsAppInboxStore}.
285
+ *
286
+ * Every handler is defensive: a malformed payload is skipped, never thrown,
287
+ * because it runs inside the socket's event loop.
288
+ */
289
+ export class WhatsAppInboxCapture {
290
+ store;
291
+ opts;
292
+ groupLookups = new Set();
293
+ /**
294
+ * @param store - Destination store
295
+ * @param opts - Own-JID accessor, group-subject fetcher, clock
296
+ */
297
+ constructor(store, opts = {}) {
298
+ this.store = store;
299
+ this.opts = opts;
300
+ }
301
+ /**
302
+ * Store one message and name its chat.
303
+ *
304
+ * @param raw - A WAMessage
305
+ * @returns True when stored
306
+ */
307
+ captureMessage(raw) {
308
+ const parsed = parseBaileysMessage(raw, this.opts.getOwnJid?.() ?? null);
309
+ if (!parsed)
310
+ return false;
311
+ const { message, isGroup, pushName } = parsed;
312
+ // In a 1:1 chat the sender's pushName is a (weak) name for the chat.
313
+ this.store.upsertChat({
314
+ id: message.chatId,
315
+ isGroup,
316
+ name: !isGroup ? pushName : null,
317
+ nameRank: WHATSAPP_CONSTANTS.NAME_RANKS.PUSH_NAME,
318
+ lastMessageAt: message.ts,
319
+ });
320
+ this.store.upsertMessage(message);
321
+ if (isGroup)
322
+ this.maybeFetchGroupName(message.chatId);
323
+ return true;
324
+ }
325
+ /**
326
+ * Handle `messages.upsert` (both `notify` and `append`: own messages sent
327
+ * from the phone and backfills arrive as `append`).
328
+ *
329
+ * @param upsert - `{ messages, type }`
330
+ * @returns Number of messages stored
331
+ */
332
+ onMessagesUpsert(upsert) {
333
+ const messages = asRec(upsert)?.messages;
334
+ if (!Array.isArray(messages))
335
+ return 0;
336
+ let n = 0;
337
+ for (const m of messages)
338
+ if (this.captureMessage(m))
339
+ n++;
340
+ return n;
341
+ }
342
+ /**
343
+ * Handle `messaging-history.set`: seed chats, contact names and recent
344
+ * messages. Messages older than `HISTORY_SYNC_MAX_AGE_MS` are skipped.
345
+ *
346
+ * @param payload - `{ chats, contacts, messages }`
347
+ * @returns Number of messages stored
348
+ */
349
+ onHistorySet(payload) {
350
+ const p = asRec(payload);
351
+ if (!p)
352
+ return 0;
353
+ const chats = (Array.isArray(p.chats) ? p.chats : []).map(chatFromChat).filter((c) => c !== null);
354
+ const contacts = (Array.isArray(p.contacts) ? p.contacts : [])
355
+ .map(chatFromContact)
356
+ .filter((c) => c !== null);
357
+ this.store.upsertChats([...chats, ...contacts]);
358
+ const cutoff = (this.opts.now?.() ?? Date.now()) - WHATSAPP_CONSTANTS.HISTORY_SYNC_MAX_AGE_MS;
359
+ const ownJid = this.opts.getOwnJid?.() ?? null;
360
+ const parsed = (Array.isArray(p.messages) ? p.messages : [])
361
+ .map((m) => parseBaileysMessage(m, ownJid))
362
+ .filter((m) => m !== null && m.message.ts >= cutoff);
363
+ this.store.upsertChats(parsed
364
+ .filter((m) => !m.isGroup && m.pushName)
365
+ .map((m) => ({ id: m.message.chatId, name: m.pushName, nameRank: WHATSAPP_CONSTANTS.NAME_RANKS.PUSH_NAME })));
366
+ return this.store.upsertMessages(parsed.map((m) => m.message));
367
+ }
368
+ /**
369
+ * Handle `contacts.upsert` / `contacts.update`.
370
+ *
371
+ * @param contacts - Contact list
372
+ */
373
+ onContacts(contacts) {
374
+ if (!Array.isArray(contacts))
375
+ return;
376
+ this.store.upsertChats(contacts.map(chatFromContact).filter((c) => c !== null));
377
+ }
378
+ /**
379
+ * Handle `chats.upsert` / `chats.update`.
380
+ *
381
+ * @param chats - Chat list
382
+ */
383
+ onChats(chats) {
384
+ if (!Array.isArray(chats))
385
+ return;
386
+ this.store.upsertChats(chats.map(chatFromChat).filter((c) => c !== null));
387
+ }
388
+ /**
389
+ * Handle `groups.upsert` / `groups.update`.
390
+ *
391
+ * @param groups - Group metadata list
392
+ */
393
+ onGroups(groups) {
394
+ if (!Array.isArray(groups))
395
+ return;
396
+ this.store.upsertChats(groups.map(chatFromGroup).filter((c) => c !== null));
397
+ }
398
+ /**
399
+ * Look a group's subject up once per process when we have no name for it.
400
+ *
401
+ * @param jid - Group JID
402
+ */
403
+ maybeFetchGroupName(jid) {
404
+ const fetcher = this.opts.fetchGroupSubject;
405
+ if (!fetcher || this.groupLookups.has(jid))
406
+ return;
407
+ if (this.store.getChat(jid)?.name)
408
+ return;
409
+ this.groupLookups.add(jid);
410
+ fetcher(jid)
411
+ .then((subject) => {
412
+ if (subject)
413
+ this.store.upsertChat({ id: jid, name: subject, nameRank: WHATSAPP_CONSTANTS.NAME_RANKS.CHAT, isGroup: true });
414
+ })
415
+ .catch(() => {
416
+ // Best effort — the chat keeps its JID as the label.
417
+ });
418
+ }
419
+ }
420
+ //# sourceMappingURL=whatsapp-inbox-capture.js.map