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.
- package/config/skills/agent/core/whatsapp-draft/SKILL.md +80 -0
- package/config/skills/agent/core/whatsapp-draft/execute.sh +84 -0
- package/config/skills/agent/core/whatsapp-draft/execute.test.sh +70 -0
- package/config/skills/agent/core/whatsapp-inbox/SKILL.md +75 -0
- package/config/skills/agent/core/whatsapp-inbox/execute.sh +87 -0
- package/config/skills/agent/core/whatsapp-inbox/execute.test.sh +68 -0
- package/config/skills/agent/core/whatsapp-read/SKILL.md +80 -0
- package/config/skills/agent/core/whatsapp-read/execute.sh +116 -0
- package/config/skills/agent/core/whatsapp-read/execute.test.sh +84 -0
- package/config/skills/agent/core/whatsapp-send/SKILL.md +70 -0
- package/config/skills/agent/core/whatsapp-send/execute.sh +84 -0
- package/config/skills/agent/core/whatsapp-send/execute.test.sh +79 -0
- package/dist/backend/backend/src/constants.d.ts +107 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +105 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts +73 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js +310 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +53 -4
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +29 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/index.d.ts +5 -1
- package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/index.js +5 -1
- package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts +59 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js +108 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts +74 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js +82 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts +184 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js +420 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts +243 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js +538 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +32 -2
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +65 -9
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +11 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +59 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +159 -9
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -1
- package/dist/backend/backend/src/types/whatsapp.types.d.ts +108 -0
- package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/whatsapp.types.js +9 -0
- package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +107 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +105 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/frontend/dist/assets/{index-07e338a9.js → index-daf43f1a.js} +268 -268
- package/frontend/dist/index.html +1 -1
- 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
|