@melandlabs/integrations-whatsapp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +28 -0
- package/dist/client-registry.d.ts +55 -0
- package/dist/client-registry.js +84 -0
- package/dist/client-registry.js.map +1 -0
- package/dist/conversation-store.d.ts +27 -0
- package/dist/conversation-store.js +84 -0
- package/dist/conversation-store.js.map +1 -0
- package/dist/index.d.ts +354 -0
- package/dist/index.js +6329 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +33 -0
- package/dist/markdown.js +37 -0
- package/dist/markdown.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { MessagePlatformAdapter, Messages, MessageTarget, MessageEvent } from '@melandlabs/integrations-channels';
|
|
2
|
+
import { ExtractedMessageInfo } from '@melandlabs/integrations-channels/sources/types';
|
|
3
|
+
import { BaileysAuthStateProvider, FileIngester, ClientRegistry, ConfigProvider } from '@melandlabs/integrations/core';
|
|
4
|
+
import { WASocket, WAMessage } from '@whiskeysockets/baileys';
|
|
5
|
+
export { WhatsAppConversationStore } from './conversation-store.js';
|
|
6
|
+
export { WhatsAppClientRegistry } from './client-registry.js';
|
|
7
|
+
export { markdownToWhatsApp } from './markdown.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* WhatsApp Adapter using @whiskeysockets/baileys (WebSocket protocol)
|
|
11
|
+
*
|
|
12
|
+
* Replaces whatsapp-web.js (Puppeteer/Chromium) with Baileys WebSocket.
|
|
13
|
+
* No browser = no automation detection surface.
|
|
14
|
+
*
|
|
15
|
+
* This adapter is platform-agnostic - web-specific dependencies are provided
|
|
16
|
+
* via interfaces (CredentialStore, FileIngester, ClientRegistry, etc.)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type WhatsAppDialogInfo = {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
type: "private" | "group";
|
|
23
|
+
};
|
|
24
|
+
type WhatsAppUserInfo = {
|
|
25
|
+
wid: string;
|
|
26
|
+
pushName?: string;
|
|
27
|
+
formattedNumber?: string;
|
|
28
|
+
};
|
|
29
|
+
type WhatsAppLoginCallbacks = {
|
|
30
|
+
onQr?: (qr: string) => Promise<void> | void;
|
|
31
|
+
onCode?: (code: string) => Promise<void> | void;
|
|
32
|
+
onSession?: (session: unknown) => Promise<void> | void;
|
|
33
|
+
onReady?: (info: WhatsAppUserInfo) => Promise<void> | void;
|
|
34
|
+
onError?: (error: Error) => Promise<void> | void;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Module-level registry of active WhatsAppAdapters by sessionId.
|
|
38
|
+
* Used by QR login to expose the adapter for socket re-registration by accountId.
|
|
39
|
+
*/
|
|
40
|
+
declare const activeAdapters: Map<string, WhatsAppAdapter>;
|
|
41
|
+
|
|
42
|
+
declare class WhatsAppAdapter extends MessagePlatformAdapter {
|
|
43
|
+
sock: WASocket | null;
|
|
44
|
+
botId: string;
|
|
45
|
+
messages: Messages;
|
|
46
|
+
name: string;
|
|
47
|
+
private sessionId;
|
|
48
|
+
private authStateProvider;
|
|
49
|
+
private authState;
|
|
50
|
+
private isReady;
|
|
51
|
+
/** In-memory chat list (replaces makeInMemoryStore in v7). Updated via chats.upsert events. */
|
|
52
|
+
private chats;
|
|
53
|
+
private asyncIteratorState;
|
|
54
|
+
/** On-demand history backfill budget, reset per getChatsByChunk cycle. */
|
|
55
|
+
private backfillRequestsThisCycle;
|
|
56
|
+
/** Persistent message store; created here when no listener attached one. */
|
|
57
|
+
private messageStore;
|
|
58
|
+
/** Override for the message store base dir (tests). */
|
|
59
|
+
private historyStoreDir?;
|
|
60
|
+
/** False when sessionId fell back to a random uuid (no botId/sessionKey). */
|
|
61
|
+
private hasStableSessionId;
|
|
62
|
+
private isAuthenticated;
|
|
63
|
+
private initializationPromise;
|
|
64
|
+
/** Exposed so other adapters (e.g. insights bot) can await socket readiness. */
|
|
65
|
+
get pendingInitialization(): Promise<void> | null;
|
|
66
|
+
private loginDeferred;
|
|
67
|
+
private ownerUserId?;
|
|
68
|
+
private ownerUserType?;
|
|
69
|
+
private eventCleanup;
|
|
70
|
+
/** Pending reconnect socket creation, used to prevent concurrent reconnects */
|
|
71
|
+
private _pendingReconnect;
|
|
72
|
+
private reconnectTimer;
|
|
73
|
+
private isClosed;
|
|
74
|
+
/** Cache of recently sent messages for msgRetry requests (max 256 entries). */
|
|
75
|
+
private sentMessageCache;
|
|
76
|
+
private fileIngester?;
|
|
77
|
+
private clientRegistry?;
|
|
78
|
+
private configProvider?;
|
|
79
|
+
constructor(opts?: {
|
|
80
|
+
botId?: string;
|
|
81
|
+
ownerUserId?: string;
|
|
82
|
+
ownerUserType?: string;
|
|
83
|
+
/** Override sessionId — used by insight bot to reuse QR login's session */
|
|
84
|
+
sessionKey?: string;
|
|
85
|
+
authStateProvider: BaileysAuthStateProvider;
|
|
86
|
+
fileIngester?: FileIngester;
|
|
87
|
+
clientRegistry?: ClientRegistry;
|
|
88
|
+
configProvider?: ConfigProvider;
|
|
89
|
+
/** Override the persistent message store base dir (used by tests) */
|
|
90
|
+
historyStoreDir?: string;
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Register the socket under an additional key (e.g. account.id).
|
|
94
|
+
* Call this after QR login succeeds and integration account is created,
|
|
95
|
+
* so self-listener and insight bot can find the socket by accountId.
|
|
96
|
+
*/
|
|
97
|
+
setRegisterSocketAs(key: string): void;
|
|
98
|
+
/**
|
|
99
|
+
* Find an active adapter by sessionId and register its socket under accountId.
|
|
100
|
+
* Called from guest-guide after integration account is created.
|
|
101
|
+
*/
|
|
102
|
+
static registerSocketByAccountId(sessionId: string, accountId: string): void;
|
|
103
|
+
private ensureAuthState;
|
|
104
|
+
/**
|
|
105
|
+
* Create and connect the Baileys socket with in-memory store.
|
|
106
|
+
* Auth state must be loaded (await ensureAuthState()) before calling this.
|
|
107
|
+
*/
|
|
108
|
+
private createSocket;
|
|
109
|
+
private registerInternalEvents;
|
|
110
|
+
private saveAuthState;
|
|
111
|
+
/**
|
|
112
|
+
* Register connection/creds listeners on an existing socket.
|
|
113
|
+
* Called when a socket is reused from the registry.
|
|
114
|
+
*/
|
|
115
|
+
setupListenersOnSocket(sock: WASocket): void;
|
|
116
|
+
private handleLoginReady;
|
|
117
|
+
private isTauriMode;
|
|
118
|
+
private timeBeforeHours;
|
|
119
|
+
/**
|
|
120
|
+
* Wait for the initial history sync to populate this.chats.
|
|
121
|
+
* History arrives via phone-pushed messaging-history.set events
|
|
122
|
+
* (syncFullHistory: true); there is no server API to request it —
|
|
123
|
+
* resyncAppState only syncs app state (mute/archive/contacts) and
|
|
124
|
+
* gets rate-limited (rate-overlimit) when called repeatedly.
|
|
125
|
+
*/
|
|
126
|
+
private waitForInitialHistorySync;
|
|
127
|
+
/**
|
|
128
|
+
* Make sure the socket carries a persistent message history store. The
|
|
129
|
+
* phone pushes the full history exactly once, seconds after pairing — so
|
|
130
|
+
* the store must be attached when the socket is created, not later by the
|
|
131
|
+
* self-message listener (whose frontend-triggered init loses that race).
|
|
132
|
+
* Keyed by sessionId so every adapter instance for the same WhatsApp
|
|
133
|
+
* session reads the same persisted data.
|
|
134
|
+
*/
|
|
135
|
+
private ensureMessageStore;
|
|
136
|
+
/**
|
|
137
|
+
* Restore this.chats from the persisted message history store (attached to
|
|
138
|
+
* the socket by the self-message listener). Lets insight refreshes work
|
|
139
|
+
* right after a restart, before any new history sync events arrive.
|
|
140
|
+
*/
|
|
141
|
+
private hydrateChatsFromStore;
|
|
142
|
+
/**
|
|
143
|
+
* On-demand history backfill via the official Baileys API: when local
|
|
144
|
+
* coverage for a chat does not reach the requested window start, page
|
|
145
|
+
* backwards from the oldest locally stored message (the required anchor)
|
|
146
|
+
* with sock.fetchMessageHistory. The phone responds asynchronously through
|
|
147
|
+
* an ON_DEMAND messaging-history.set, which the attached store persists —
|
|
148
|
+
* so arrival is detected by polling the store's oldest message.
|
|
149
|
+
*/
|
|
150
|
+
private backfillChatHistory;
|
|
151
|
+
/**
|
|
152
|
+
* Wait for the phone's answer to a fetchMessageHistory request: either the
|
|
153
|
+
* store's oldest message gets older (new history arrived), or an ON_DEMAND
|
|
154
|
+
* messaging-history.set lands without older messages (end of history).
|
|
155
|
+
*/
|
|
156
|
+
private waitForBackfillResponse;
|
|
157
|
+
private ensureReady;
|
|
158
|
+
startQrLogin(callbacks?: WhatsAppLoginCallbacks): Promise<string>;
|
|
159
|
+
startPairingCodeLogin(phoneNumber: string, callbacks?: WhatsAppLoginCallbacks): Promise<string>;
|
|
160
|
+
getUserInfo(): WhatsAppUserInfo;
|
|
161
|
+
sendMessages(target: MessageTarget, id: string, messages: Messages): Promise<void>;
|
|
162
|
+
sendMessage(target: MessageTarget, id: string, message: string): Promise<void>;
|
|
163
|
+
replyMessages(event: MessageEvent, messages: Messages, quoteOrigin?: boolean): Promise<void>;
|
|
164
|
+
run(): Promise<void>;
|
|
165
|
+
isUsingLocalAuth(): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Start the socket without waiting for connection (for self-listener use).
|
|
168
|
+
* Creates the socket and registers it in the client registry.
|
|
169
|
+
* Always sets up listeners (even when socket was already connected)
|
|
170
|
+
* so callers receive events.
|
|
171
|
+
*/
|
|
172
|
+
startSocket(): Promise<WASocket>;
|
|
173
|
+
/**
|
|
174
|
+
* Attach this adapter to an already-connected socket (from registry).
|
|
175
|
+
* Used by QR login to reuse the self-listener's socket instead of creating a new one.
|
|
176
|
+
*/
|
|
177
|
+
attachToSocket(sock: WASocket): void;
|
|
178
|
+
/**
|
|
179
|
+
* Kill the socket. If targetSock is provided, only kill that socket's listeners
|
|
180
|
+
* (used during reconnect to avoid killing the new socket's listeners).
|
|
181
|
+
* If no targetSock, kills the current socket.
|
|
182
|
+
*/
|
|
183
|
+
kill(targetSock?: WASocket): Promise<boolean>;
|
|
184
|
+
getSessionIdentifier(): string;
|
|
185
|
+
getDialogs(): Promise<WhatsAppDialogInfo[]>;
|
|
186
|
+
getChatsByChunk(since: number, chunkSize?: number): Promise<{
|
|
187
|
+
messages: ExtractedMessageInfo[];
|
|
188
|
+
hasMore: boolean;
|
|
189
|
+
}>;
|
|
190
|
+
getChatsByChunkHours(hours?: number): Promise<{
|
|
191
|
+
messages: ExtractedMessageInfo[];
|
|
192
|
+
hasMore: boolean;
|
|
193
|
+
}>;
|
|
194
|
+
getChatsByTime(cutoffDate: number): Promise<ExtractedMessageInfo[]>;
|
|
195
|
+
getChatsByHours(hours?: number): Promise<ExtractedMessageInfo[]>;
|
|
196
|
+
getChatsByDays(days?: number): Promise<ExtractedMessageInfo[]>;
|
|
197
|
+
resetChunkIterator(): void;
|
|
198
|
+
private extractMessageInfo;
|
|
199
|
+
private getMessageText;
|
|
200
|
+
private resolveChatId;
|
|
201
|
+
private prepareMediaMessage;
|
|
202
|
+
private prepareFileMessage;
|
|
203
|
+
private guessMimeType;
|
|
204
|
+
authStateExists(): Promise<boolean>;
|
|
205
|
+
/**
|
|
206
|
+
* @deprecated Use authStateExists instead
|
|
207
|
+
*/
|
|
208
|
+
sessionExists(): Promise<boolean>;
|
|
209
|
+
forceSaveSession(): Promise<void>;
|
|
210
|
+
/** Save session to disk. Call after QR login completes. */
|
|
211
|
+
saveSession(): Promise<void>;
|
|
212
|
+
checkSessionStatus(): Promise<{
|
|
213
|
+
exists: boolean;
|
|
214
|
+
ready: boolean;
|
|
215
|
+
authenticated: boolean;
|
|
216
|
+
}>;
|
|
217
|
+
restoreSession(): Promise<boolean>;
|
|
218
|
+
ingestMessageAttachments(msg: WAMessage): Promise<any[]>;
|
|
219
|
+
getSocket(): WASocket | null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* WhatsApp Message History Store
|
|
224
|
+
*
|
|
225
|
+
* File-backed replacement for the minimal in-memory Baileys store (Baileys v7
|
|
226
|
+
* removed makeInMemoryStore). Full WAMessage objects stay in memory for
|
|
227
|
+
* real-time consumers; a sanitized copy (binary payloads dropped, Long
|
|
228
|
+
* timestamps normalized to numbers) is persisted per chat so message history
|
|
229
|
+
* and the chat list survive process restarts. Persisted messages also provide
|
|
230
|
+
* the anchor (oldest message key + timestamp) required by Baileys'
|
|
231
|
+
* sock.fetchMessageHistory on-demand backfill API.
|
|
232
|
+
*
|
|
233
|
+
* Layout:
|
|
234
|
+
* <baseDir>/<accountId>/chats.json
|
|
235
|
+
* <baseDir>/<accountId>/messages/<base64url(jid)>.json
|
|
236
|
+
*
|
|
237
|
+
* ARCHITECTURE EXCEPTION (per repo rule: exceptions must be explicit).
|
|
238
|
+
* Data is stored as plain JSON, unencrypted.
|
|
239
|
+
* - Why: matches the Baileys auth state in local/Tauri mode, which already
|
|
240
|
+
* sits unencrypted in the same data dir and grants full account access.
|
|
241
|
+
* - Impact scope: in cloud deployments the auth state lives in Redis, so this
|
|
242
|
+
* directory is an additional at-rest exposure of message content on the
|
|
243
|
+
* server filesystem.
|
|
244
|
+
* - Temporary: yes — the follow-up is to encrypt at rest in cloud mode using
|
|
245
|
+
* packages/security (key management decision pending), or scope this store
|
|
246
|
+
* to local/Tauri mode only.
|
|
247
|
+
* - Mitigation today: per-chat cap (500 msgs), binary payloads stripped, and
|
|
248
|
+
* mandatory lifecycle cleanup — callers must invoke
|
|
249
|
+
* WhatsAppMessageHistoryStore.purgeAccountData() when the integration
|
|
250
|
+
* account is disconnected so no message content outlives the account
|
|
251
|
+
* (purge also destroys live in-process instances; see tests in
|
|
252
|
+
* message-history-store.test.ts).
|
|
253
|
+
* - Known gaps for the encryption follow-up: whole-user account deletion
|
|
254
|
+
* paths that bypass integration disconnect do not purge; in multi-instance
|
|
255
|
+
* cloud deployments purge only clears the current instance's filesystem.
|
|
256
|
+
*
|
|
257
|
+
* Limitation: sanitization strips media keys/thumbnails, so messages
|
|
258
|
+
* hydrated from disk cannot be passed to downloadMediaMessage — only text
|
|
259
|
+
* extraction and backfill anchoring work on persisted history.
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
type PersistedChatInfo = {
|
|
263
|
+
id: string;
|
|
264
|
+
name?: string;
|
|
265
|
+
};
|
|
266
|
+
declare class WhatsAppMessageHistoryStore {
|
|
267
|
+
private readonly accountDir;
|
|
268
|
+
private readonly messagesDir;
|
|
269
|
+
private messages;
|
|
270
|
+
/** Per-chat id sets mirroring `messages`, for O(1) dedup on insert. */
|
|
271
|
+
private messageIds;
|
|
272
|
+
private hydratedJids;
|
|
273
|
+
private chats;
|
|
274
|
+
private chatsHydrated;
|
|
275
|
+
private dirtyJids;
|
|
276
|
+
private chatsDirty;
|
|
277
|
+
private flushTimer;
|
|
278
|
+
private consecutiveFlushFailures;
|
|
279
|
+
private closed;
|
|
280
|
+
private readonly persistEnabled;
|
|
281
|
+
/** ON_DEMAND history responses seen — backfill's "phone answered" signal. */
|
|
282
|
+
private onDemandResponses;
|
|
283
|
+
/**
|
|
284
|
+
* Delete all persisted history for an account. Must be called when the
|
|
285
|
+
* integration account is disconnected/removed so message content does not
|
|
286
|
+
* outlive the account on disk. Live store instances for the account are
|
|
287
|
+
* destroyed first — otherwise their pending debounced flushes (or messages
|
|
288
|
+
* still arriving on an undisposed socket) would resurrect the files.
|
|
289
|
+
*/
|
|
290
|
+
static purgeAccountData(accountId: string, baseDir?: string): void;
|
|
291
|
+
constructor(opts: {
|
|
292
|
+
accountId: string;
|
|
293
|
+
baseDir?: string;
|
|
294
|
+
persist?: boolean;
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* Permanently disable this store: stop pending flushes, drop in-memory
|
|
298
|
+
* data, and turn all writes into no-ops. Called by purgeAccountData so a
|
|
299
|
+
* disconnected account's messages cannot be re-persisted.
|
|
300
|
+
*/
|
|
301
|
+
/** True once destroy() has run — writes are no-ops; do not attach/reuse. */
|
|
302
|
+
get isClosed(): boolean;
|
|
303
|
+
destroy(): void;
|
|
304
|
+
/**
|
|
305
|
+
* Attach event listeners to a WASocket to populate the store. History
|
|
306
|
+
* arrives via phone-pushed messaging-history.set (initial pairing and
|
|
307
|
+
* ON_DEMAND backfill responses); real-time messages via messages.upsert.
|
|
308
|
+
*/
|
|
309
|
+
attach(sock: WASocket): void;
|
|
310
|
+
addMessages(msgs: WAMessage[]): void;
|
|
311
|
+
addChats(chats: Array<{
|
|
312
|
+
id?: string | null;
|
|
313
|
+
name?: string | null;
|
|
314
|
+
}>): void;
|
|
315
|
+
/** Same interface as the old in-memory store (used by Baileys consumers). */
|
|
316
|
+
loadMessages(jid: string, count: number, _opts: object): Promise<WAMessage[]>;
|
|
317
|
+
/** Chronologically oldest stored message — the fetchMessageHistory anchor. */
|
|
318
|
+
getOldestMessage(jid: string): WAMessage | undefined;
|
|
319
|
+
/**
|
|
320
|
+
* Number of ON_DEMAND history sync responses received since this store
|
|
321
|
+
* instance was created. Used by backfill to distinguish "phone answered
|
|
322
|
+
* with nothing older" (end of history) from "phone never answered".
|
|
323
|
+
*/
|
|
324
|
+
getOnDemandResponseCount(): number;
|
|
325
|
+
/**
|
|
326
|
+
* False once the per-chat cap is reached: addMessages trims the oldest
|
|
327
|
+
* entries, so backfilled history would be discarded immediately and the
|
|
328
|
+
* oldest anchor would never move — requesting more is pointless.
|
|
329
|
+
*/
|
|
330
|
+
canStoreOlderMessages(jid: string): boolean;
|
|
331
|
+
/** Persisted chat list, used to rebuild adapter state after a restart. */
|
|
332
|
+
loadChats(): PersistedChatInfo[];
|
|
333
|
+
/** Write all pending changes to disk immediately. */
|
|
334
|
+
flush(): void;
|
|
335
|
+
/**
|
|
336
|
+
* Write up to maxFiles dirty chats. The initial full history sync can dirty
|
|
337
|
+
* hundreds of chats at once; writing them all synchronously in one tick
|
|
338
|
+
* would stall the event loop right in the middle of onboarding, so the
|
|
339
|
+
* debounced timer writes in batches and reschedules for the remainder.
|
|
340
|
+
* Explicit flush() still drains everything (teardown must not lose data).
|
|
341
|
+
*/
|
|
342
|
+
private writePending;
|
|
343
|
+
/**
|
|
344
|
+
* Write via temp file + rename so a crash can't leave truncated JSON.
|
|
345
|
+
* The pid suffix keeps multi-worker processes from racing on the same
|
|
346
|
+
* temp path (each still last-write-wins on the final file).
|
|
347
|
+
*/
|
|
348
|
+
private writeFileAtomic;
|
|
349
|
+
private scheduleFlush;
|
|
350
|
+
private hydrate;
|
|
351
|
+
private hydrateChats;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export { type PersistedChatInfo, WhatsAppAdapter, type WhatsAppDialogInfo, WhatsAppMessageHistoryStore, type WhatsAppUserInfo, activeAdapters };
|