@lofa199419/waha-v2 2.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/src/client.ts ADDED
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Typed wrapper over waha-node's WahaClient.
3
+ *
4
+ * waha-node uses `any` everywhere; this file provides typed entry points
5
+ * for the operations the plugin actually uses so the rest of the codebase
6
+ * stays `any`-free.
7
+ */
8
+ import { WahaClient } from "waha-node";
9
+ import type { WahaV2MessageResult, WahaV2SessionInfo } from "./types.js";
10
+
11
+ export type WahaV2ClientConfig = {
12
+ baseUrl: string;
13
+ apiKey?: string;
14
+ };
15
+
16
+ export type WahaV2FilePayload = {
17
+ data: string; // base64
18
+ mimetype: string;
19
+ filename: string;
20
+ };
21
+
22
+ export type WahaV2PollPayload = {
23
+ name: string;
24
+ options: string[];
25
+ multipleAnswers?: boolean;
26
+ };
27
+
28
+ export class WahaV2Client {
29
+ private readonly inner: WahaClient;
30
+ private readonly _config: WahaV2ClientConfig;
31
+
32
+ constructor(config: WahaV2ClientConfig) {
33
+ this._config = config;
34
+ this.inner = new WahaClient(config.baseUrl, config.apiKey);
35
+ }
36
+
37
+ private async postPresence(
38
+ endpoint: "/api/startTyping" | "/api/stopTyping",
39
+ session: string,
40
+ chatId: string,
41
+ ): Promise<void> {
42
+ const url = `${this._config.baseUrl}${endpoint}`;
43
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
44
+ if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
45
+ const response = await fetch(url, {
46
+ method: "POST",
47
+ headers,
48
+ body: JSON.stringify({ session, chatId }),
49
+ });
50
+ if (response.ok) {
51
+ return;
52
+ }
53
+ const body = await response.text().catch(() => "");
54
+ const suffix = body ? ` body=${body.slice(0, 240)}` : "";
55
+ throw new Error(`WAHA ${endpoint} failed: ${response.status} ${response.statusText}${suffix}`);
56
+ }
57
+
58
+ async getChatLabels(session: string, chatId: string): Promise<Array<{ id?: string; name?: string }>> {
59
+ const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/labels/chats/${encodeURIComponent(chatId)}`;
60
+ const headers: Record<string, string> = { Accept: "application/json" };
61
+ if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
62
+ const response = await fetch(url, { method: "GET", headers });
63
+ if (!response.ok) {
64
+ const body = await response.text().catch(() => "");
65
+ const suffix = body ? ` body=${body.slice(0, 240)}` : "";
66
+ throw new Error(
67
+ `WAHA /labels/chats lookup failed: ${response.status} ${response.statusText}${suffix}`,
68
+ );
69
+ }
70
+ const json = (await response.json().catch(() => [])) as unknown;
71
+ if (!Array.isArray(json)) return [];
72
+ return json as Array<{ id?: string; name?: string }>;
73
+ }
74
+
75
+ async listLabels(
76
+ session: string,
77
+ ): Promise<Array<{ id?: string; name?: string; color?: number; colorHex?: string }>> {
78
+ const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/labels`;
79
+ const headers: Record<string, string> = { Accept: "application/json" };
80
+ if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
81
+ const response = await fetch(url, { method: "GET", headers });
82
+ if (!response.ok) {
83
+ const body = await response.text().catch(() => "");
84
+ const suffix = body ? ` body=${body.slice(0, 240)}` : "";
85
+ throw new Error(`WAHA /labels lookup failed: ${response.status} ${response.statusText}${suffix}`);
86
+ }
87
+ const json = (await response.json().catch(() => [])) as unknown;
88
+ if (!Array.isArray(json)) return [];
89
+ return json as Array<{ id?: string; name?: string; color?: number; colorHex?: string }>;
90
+ }
91
+
92
+ async setChatLabels(session: string, chatId: string, labelIds: string[]): Promise<void> {
93
+ const url = `${this._config.baseUrl}/api/${encodeURIComponent(session)}/labels/chats/${encodeURIComponent(chatId)}`;
94
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
95
+ if (this._config.apiKey) headers["X-Api-Key"] = this._config.apiKey;
96
+ const response = await fetch(url, {
97
+ method: "PUT",
98
+ headers,
99
+ body: JSON.stringify({ labels: labelIds }),
100
+ });
101
+ if (!response.ok) {
102
+ const body = await response.text().catch(() => "");
103
+ const suffix = body ? ` body=${body.slice(0, 240)}` : "";
104
+ throw new Error(
105
+ `WAHA /labels/chats update failed: ${response.status} ${response.statusText}${suffix}`,
106
+ );
107
+ }
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Session management
112
+ // ---------------------------------------------------------------------------
113
+
114
+ async listSessions(all = true): Promise<WahaV2SessionInfo[]> {
115
+ const result = (await this.inner.sessions.list(all)) as WahaV2SessionInfo[];
116
+ return Array.isArray(result) ? result : [];
117
+ }
118
+
119
+ /**
120
+ * Create a session, optionally embedding webhook config at creation time.
121
+ * Passing webhookConfig avoids a separate update call (and its timing race).
122
+ */
123
+ async createSession(session: string, webhookConfig?: Record<string, unknown>): Promise<void> {
124
+ await this.inner.sessions.create(session, webhookConfig ?? null, true);
125
+ }
126
+
127
+ /**
128
+ * Update a session's configuration (e.g. to register a webhook URL).
129
+ * WAHA docs: PUT /api/sessions/{session} with { config: { ... } }
130
+ */
131
+ async updateSession(session: string, config: Record<string, unknown>): Promise<void> {
132
+ await this.inner.sessions.update(session, config);
133
+ }
134
+
135
+ async startSession(session: string): Promise<void> {
136
+ await this.inner.sessions.start(session);
137
+ }
138
+
139
+ async stopSession(session: string): Promise<void> {
140
+ await this.inner.sessions.stop(session);
141
+ }
142
+
143
+ async logoutSession(session: string): Promise<void> {
144
+ await this.inner.sessions.logout(session);
145
+ }
146
+
147
+ async getQr(session: string): Promise<{ data?: string }> {
148
+ // WAHA returns { value: "..." } for raw format; map to { data } for consistency.
149
+ const raw = (await this.inner.sessions.getQr(session, "raw", true)) as {
150
+ value?: string;
151
+ data?: string;
152
+ };
153
+ return { data: raw?.data ?? raw?.value };
154
+ }
155
+
156
+ async requestCode(session: string, phoneNumber: string): Promise<{ code?: string }> {
157
+ return (await this.inner.sessions.requestCode(session, phoneNumber)) as { code?: string };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Messages — sending
162
+ // ---------------------------------------------------------------------------
163
+
164
+ async sendText(session: string, chatId: string, text: string): Promise<WahaV2MessageResult> {
165
+ return (await this.inner.messages.sendText(session, chatId, text)) as WahaV2MessageResult;
166
+ }
167
+
168
+ async sendImage(
169
+ session: string,
170
+ chatId: string,
171
+ file: WahaV2FilePayload,
172
+ caption?: string,
173
+ ): Promise<WahaV2MessageResult> {
174
+ return (await this.inner.messages.sendImage(
175
+ session,
176
+ chatId,
177
+ file,
178
+ caption,
179
+ )) as WahaV2MessageResult;
180
+ }
181
+
182
+ async sendVideo(
183
+ session: string,
184
+ chatId: string,
185
+ file: WahaV2FilePayload,
186
+ caption?: string,
187
+ ): Promise<WahaV2MessageResult> {
188
+ return (await this.inner.messages.sendVideo(
189
+ session,
190
+ chatId,
191
+ file,
192
+ caption,
193
+ )) as WahaV2MessageResult;
194
+ }
195
+
196
+ async sendVoice(
197
+ session: string,
198
+ chatId: string,
199
+ file: WahaV2FilePayload,
200
+ ): Promise<WahaV2MessageResult> {
201
+ return (await this.inner.messages.sendVoice(session, chatId, file)) as WahaV2MessageResult;
202
+ }
203
+
204
+ async sendFile(
205
+ session: string,
206
+ chatId: string,
207
+ file: WahaV2FilePayload,
208
+ caption?: string,
209
+ ): Promise<WahaV2MessageResult> {
210
+ return (await this.inner.messages.sendFile(
211
+ session,
212
+ chatId,
213
+ file,
214
+ caption,
215
+ )) as WahaV2MessageResult;
216
+ }
217
+
218
+ async sendLocation(
219
+ session: string,
220
+ chatId: string,
221
+ latitude: number,
222
+ longitude: number,
223
+ title?: string,
224
+ ): Promise<WahaV2MessageResult> {
225
+ return (await this.inner.messages.sendLocation(
226
+ session,
227
+ chatId,
228
+ latitude,
229
+ longitude,
230
+ title,
231
+ )) as WahaV2MessageResult;
232
+ }
233
+
234
+ /** contacts is an array of vCard-like objects: [{ vcard: "BEGIN:VCARD..." }] or phone JIDs */
235
+ async sendContact(
236
+ session: string,
237
+ chatId: string,
238
+ contacts: unknown[],
239
+ ): Promise<WahaV2MessageResult> {
240
+ return (await this.inner.messages.sendContact(
241
+ session,
242
+ chatId,
243
+ contacts,
244
+ )) as WahaV2MessageResult;
245
+ }
246
+
247
+ async sendPoll(
248
+ session: string,
249
+ chatId: string,
250
+ poll: WahaV2PollPayload,
251
+ ): Promise<WahaV2MessageResult> {
252
+ return (await this.inner.messages.sendPoll(session, chatId, poll)) as WahaV2MessageResult;
253
+ }
254
+
255
+ async forwardMessage(
256
+ session: string,
257
+ chatId: string,
258
+ messageId: string,
259
+ ): Promise<WahaV2MessageResult> {
260
+ return (await this.inner.messages.forwardMessage(
261
+ session,
262
+ chatId,
263
+ messageId,
264
+ )) as WahaV2MessageResult;
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Messages — management
269
+ // ---------------------------------------------------------------------------
270
+
271
+ async addReaction(session: string, messageId: string, reaction: string): Promise<void> {
272
+ await this.inner.messages.addReaction(session, messageId, reaction);
273
+ }
274
+
275
+ async starMessage(
276
+ session: string,
277
+ chatId: string,
278
+ messageId: string,
279
+ star = true,
280
+ ): Promise<void> {
281
+ await this.inner.messages.starMessage(session, chatId, messageId, star);
282
+ }
283
+
284
+ async editMessage(
285
+ session: string,
286
+ chatId: string,
287
+ messageId: string,
288
+ text: string,
289
+ ): Promise<void> {
290
+ await this.inner.messages.editMessage(session, chatId, messageId, text);
291
+ }
292
+
293
+ async deleteMessage(session: string, chatId: string, messageId: string): Promise<void> {
294
+ await this.inner.messages.deleteMessage(session, chatId, messageId);
295
+ }
296
+
297
+ async pinMessage(session: string, chatId: string, messageId: string): Promise<void> {
298
+ await this.inner.messages.pinMessage(session, chatId, messageId);
299
+ }
300
+
301
+ async unpinMessage(session: string, chatId: string, messageId: string): Promise<void> {
302
+ await this.inner.messages.unpinMessage(session, chatId, messageId);
303
+ }
304
+
305
+ async sendSeen(session: string, chatId: string, messageIds?: string[]): Promise<void> {
306
+ await this.inner.messages.sendSeen(session, chatId, messageIds);
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Presence / typing — not in waha-node; call REST API directly
311
+ // ---------------------------------------------------------------------------
312
+
313
+ /** POST /api/startTyping — shows "typing…" indicator in the chat. */
314
+ async startTyping(session: string, chatId: string): Promise<void> {
315
+ await this.postPresence("/api/startTyping", session, chatId);
316
+ }
317
+
318
+ /** POST /api/stopTyping — clears the typing indicator. */
319
+ async stopTyping(session: string, chatId: string): Promise<void> {
320
+ await this.postPresence("/api/stopTyping", session, chatId);
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Chats
325
+ // ---------------------------------------------------------------------------
326
+
327
+ async listChats(session: string, limit?: number, offset?: number): Promise<unknown[]> {
328
+ return (await this.inner.chats.list(session, limit, offset)) as unknown[];
329
+ }
330
+
331
+ async getChatsOverview(session: string): Promise<unknown> {
332
+ return this.inner.chats.getOverview(session);
333
+ }
334
+
335
+ async getChatMessages(
336
+ session: string,
337
+ chatId: string,
338
+ limit?: number,
339
+ downloadMedia = false,
340
+ ): Promise<unknown[]> {
341
+ return (await this.inner.chats.getMessages(session, chatId, limit, downloadMedia)) as unknown[];
342
+ }
343
+
344
+ async getChatMessage(
345
+ session: string,
346
+ chatId: string,
347
+ messageId: string,
348
+ downloadMedia = false,
349
+ ): Promise<unknown> {
350
+ return this.inner.chats.getMessage(session, chatId, messageId, downloadMedia);
351
+ }
352
+
353
+ async readChatMessages(session: string, chatId: string, messageIds?: string[]): Promise<void> {
354
+ await this.inner.chats.readMessages(session, chatId, messageIds);
355
+ }
356
+
357
+ async archiveChat(session: string, chatId: string): Promise<void> {
358
+ await this.inner.chats.archive(session, chatId);
359
+ }
360
+
361
+ async unarchiveChat(session: string, chatId: string): Promise<void> {
362
+ await this.inner.chats.unarchive(session, chatId);
363
+ }
364
+
365
+ async deleteChat(session: string, chatId: string): Promise<void> {
366
+ await this.inner.chats.deleteChat(session, chatId);
367
+ }
368
+
369
+ async markChatUnread(session: string, chatId: string): Promise<void> {
370
+ await this.inner.chats.unread(session, chatId);
371
+ }
372
+
373
+ // ---------------------------------------------------------------------------
374
+ // Contacts
375
+ // ---------------------------------------------------------------------------
376
+
377
+ async listContacts(session: string, limit?: number, offset?: number): Promise<unknown[]> {
378
+ return (await this.inner.contacts.listAll(session, limit, offset)) as unknown[];
379
+ }
380
+
381
+ async getContact(session: string, contactId: string): Promise<unknown> {
382
+ return this.inner.contacts.getContact(session, contactId);
383
+ }
384
+
385
+ async checkContactExists(session: string, phone: string): Promise<unknown> {
386
+ return this.inner.contacts.checkExists(session, phone);
387
+ }
388
+
389
+ async getContactAbout(session: string, contactId: string): Promise<unknown> {
390
+ return this.inner.contacts.getAbout(session, contactId);
391
+ }
392
+
393
+ async getContactProfilePicture(session: string, contactId: string): Promise<unknown> {
394
+ return this.inner.contacts.getProfilePicture(session, contactId);
395
+ }
396
+
397
+ async blockContact(session: string, chatId: string): Promise<void> {
398
+ await this.inner.contacts.block(session, chatId);
399
+ }
400
+
401
+ async unblockContact(session: string, chatId: string): Promise<void> {
402
+ await this.inner.contacts.unblock(session, chatId);
403
+ }
404
+
405
+ async updateContact(
406
+ session: string,
407
+ chatId: string,
408
+ firstName: string,
409
+ lastName: string,
410
+ ): Promise<unknown> {
411
+ return this.inner.contacts.update(session, chatId, firstName, lastName);
412
+ }
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // Groups
416
+ // ---------------------------------------------------------------------------
417
+
418
+ async listGroups(session: string): Promise<unknown[]> {
419
+ return (await this.inner.groups.list(session)) as unknown[];
420
+ }
421
+
422
+ async getGroup(session: string, groupId: string): Promise<unknown> {
423
+ return this.inner.groups.getGroup(session, groupId);
424
+ }
425
+
426
+ async createGroup(session: string, subject: string, participants?: string[]): Promise<unknown> {
427
+ return this.inner.groups.create(session, subject, participants);
428
+ }
429
+
430
+ async leaveGroup(session: string, groupId: string): Promise<void> {
431
+ await this.inner.groups.leave(session, groupId);
432
+ }
433
+
434
+ async getGroupParticipants(session: string, groupId: string): Promise<unknown[]> {
435
+ return (await this.inner.groups.getParticipants(session, groupId)) as unknown[];
436
+ }
437
+
438
+ async addGroupParticipants(
439
+ session: string,
440
+ groupId: string,
441
+ participants: string[],
442
+ ): Promise<unknown> {
443
+ return this.inner.groups.addParticipants(session, groupId, participants);
444
+ }
445
+
446
+ async removeGroupParticipants(
447
+ session: string,
448
+ groupId: string,
449
+ participants: string[],
450
+ ): Promise<unknown> {
451
+ return this.inner.groups.removeParticipants(session, groupId, participants);
452
+ }
453
+
454
+ async promoteGroupAdmin(
455
+ session: string,
456
+ groupId: string,
457
+ participants: string[],
458
+ ): Promise<unknown> {
459
+ return this.inner.groups.promoteAdmin(session, groupId, participants);
460
+ }
461
+
462
+ async demoteGroupAdmin(
463
+ session: string,
464
+ groupId: string,
465
+ participants: string[],
466
+ ): Promise<unknown> {
467
+ return this.inner.groups.demoteAdmin(session, groupId, participants);
468
+ }
469
+
470
+ async getGroupInviteCode(session: string, groupId: string): Promise<unknown> {
471
+ return this.inner.groups.getInviteCode(session, groupId);
472
+ }
473
+
474
+ async revokeGroupInviteCode(session: string, groupId: string): Promise<unknown> {
475
+ return this.inner.groups.revokeInviteCode(session, groupId);
476
+ }
477
+
478
+ async updateGroupSubject(session: string, groupId: string, subject: string): Promise<void> {
479
+ await this.inner.groups.updateSubject(session, groupId, subject);
480
+ }
481
+
482
+ async updateGroupDescription(
483
+ session: string,
484
+ groupId: string,
485
+ description: string,
486
+ ): Promise<void> {
487
+ await this.inner.groups.updateDescription(session, groupId, description);
488
+ }
489
+
490
+ // ---------------------------------------------------------------------------
491
+ // Status / Stories
492
+ // ---------------------------------------------------------------------------
493
+
494
+ async sendStatusText(session: string, text: string): Promise<WahaV2MessageResult> {
495
+ return (await this.inner.status.sendText(session, text)) as WahaV2MessageResult;
496
+ }
497
+
498
+ async sendStatusImage(
499
+ session: string,
500
+ file: WahaV2FilePayload,
501
+ caption?: string,
502
+ ): Promise<WahaV2MessageResult> {
503
+ return (await this.inner.status.sendImage(session, file, caption)) as WahaV2MessageResult;
504
+ }
505
+
506
+ async sendStatusVideo(
507
+ session: string,
508
+ file: WahaV2FilePayload,
509
+ caption?: string,
510
+ ): Promise<WahaV2MessageResult> {
511
+ return (await this.inner.status.sendVideo(session, file, caption)) as WahaV2MessageResult;
512
+ }
513
+
514
+ async sendStatusVoice(session: string, file: WahaV2FilePayload): Promise<WahaV2MessageResult> {
515
+ return (await this.inner.status.sendVoice(session, file)) as WahaV2MessageResult;
516
+ }
517
+
518
+ async deleteStatus(session: string, messageId: string): Promise<void> {
519
+ await this.inner.status.deleteStatus(session, messageId);
520
+ }
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // WhatsApp Channels (Newsletters)
524
+ // ---------------------------------------------------------------------------
525
+
526
+ async listWaChannels(session: string): Promise<unknown[]> {
527
+ return (await this.inner.channels.list(session)) as unknown[];
528
+ }
529
+
530
+ async getWaChannel(session: string, channelId: string): Promise<unknown> {
531
+ return this.inner.channels.getChannel(session, channelId);
532
+ }
533
+
534
+ async createWaChannel(session: string, name: string, description?: string): Promise<unknown> {
535
+ return this.inner.channels.create(session, name, description);
536
+ }
537
+
538
+ async deleteWaChannel(session: string, channelId: string): Promise<void> {
539
+ await this.inner.channels.deleteChannel(session, channelId);
540
+ }
541
+
542
+ async getWaChannelMessages(
543
+ session: string,
544
+ channelId: string,
545
+ limit?: number,
546
+ ): Promise<unknown[]> {
547
+ return (await this.inner.channels.getMessages(session, channelId, limit)) as unknown[];
548
+ }
549
+
550
+ /**
551
+ * Download media from a WAHA media URL, authenticating with the API key.
552
+ * WAHA media URLs are internal to the WAHA server and require the API key header.
553
+ */
554
+ async downloadMediaBuffer(url: string): Promise<{ buffer: Buffer; contentType: string }> {
555
+ const { default: https } = await import("node:https");
556
+ const { default: http } = await import("node:http");
557
+ return new Promise((resolve, reject) => {
558
+ const mod = url.startsWith("https") ? https : http;
559
+ const apiKey = this._config.apiKey;
560
+ const req = mod.get(url, { headers: apiKey ? { "X-Api-Key": apiKey } : {} }, (res) => {
561
+ if (res.statusCode && res.statusCode >= 400) {
562
+ reject(new Error(`WAHA media download failed: ${res.statusCode}`));
563
+ return;
564
+ }
565
+ const chunks: Buffer[] = [];
566
+ res.on("data", (c: Buffer) => chunks.push(c));
567
+ res.on("end", () => {
568
+ resolve({
569
+ buffer: Buffer.concat(chunks),
570
+ contentType: String(res.headers["content-type"] ?? "application/octet-stream"),
571
+ });
572
+ });
573
+ });
574
+ req.on("error", reject);
575
+ req.setTimeout(30_000, () => {
576
+ req.destroy();
577
+ reject(new Error("WAHA media download timed out"));
578
+ });
579
+ });
580
+ }
581
+ }
582
+
583
+ export function createWahaV2Client(config: WahaV2ClientConfig): WahaV2Client {
584
+ return new WahaV2Client(config);
585
+ }