@kangwifi-pro/waliwa 1.0.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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +600 -0
  3. package/dist/advanced/features.d.ts +211 -0
  4. package/dist/advanced/features.d.ts.map +1 -0
  5. package/dist/advanced/features.js +671 -0
  6. package/dist/advanced/features.js.map +1 -0
  7. package/dist/auth/auth-state.d.ts +89 -0
  8. package/dist/auth/auth-state.d.ts.map +1 -0
  9. package/dist/auth/auth-state.js +330 -0
  10. package/dist/auth/auth-state.js.map +1 -0
  11. package/dist/auth/pairing-code.d.ts +78 -0
  12. package/dist/auth/pairing-code.d.ts.map +1 -0
  13. package/dist/auth/pairing-code.js +191 -0
  14. package/dist/auth/pairing-code.js.map +1 -0
  15. package/dist/auth/qr-code.d.ts +60 -0
  16. package/dist/auth/qr-code.d.ts.map +1 -0
  17. package/dist/auth/qr-code.js +151 -0
  18. package/dist/auth/qr-code.js.map +1 -0
  19. package/dist/calls/handler.d.ts +61 -0
  20. package/dist/calls/handler.d.ts.map +1 -0
  21. package/dist/calls/handler.js +117 -0
  22. package/dist/calls/handler.js.map +1 -0
  23. package/dist/core/binary.d.ts +74 -0
  24. package/dist/core/binary.d.ts.map +1 -0
  25. package/dist/core/binary.js +448 -0
  26. package/dist/core/binary.js.map +1 -0
  27. package/dist/core/crypto.d.ts +60 -0
  28. package/dist/core/crypto.d.ts.map +1 -0
  29. package/dist/core/crypto.js +170 -0
  30. package/dist/core/crypto.js.map +1 -0
  31. package/dist/core/noise.d.ts +106 -0
  32. package/dist/core/noise.d.ts.map +1 -0
  33. package/dist/core/noise.js +307 -0
  34. package/dist/core/noise.js.map +1 -0
  35. package/dist/events/emitter.d.ts +44 -0
  36. package/dist/events/emitter.d.ts.map +1 -0
  37. package/dist/events/emitter.js +79 -0
  38. package/dist/events/emitter.js.map +1 -0
  39. package/dist/features/index.d.ts +342 -0
  40. package/dist/features/index.d.ts.map +1 -0
  41. package/dist/features/index.js +755 -0
  42. package/dist/features/index.js.map +1 -0
  43. package/dist/fixes/index.d.ts +333 -0
  44. package/dist/fixes/index.d.ts.map +1 -0
  45. package/dist/fixes/index.js +762 -0
  46. package/dist/fixes/index.js.map +1 -0
  47. package/dist/groups/management.d.ts +86 -0
  48. package/dist/groups/management.d.ts.map +1 -0
  49. package/dist/groups/management.js +443 -0
  50. package/dist/groups/management.js.map +1 -0
  51. package/dist/index.d.ts +65 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +236 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/messages/media.d.ts +93 -0
  56. package/dist/messages/media.d.ts.map +1 -0
  57. package/dist/messages/media.js +252 -0
  58. package/dist/messages/media.js.map +1 -0
  59. package/dist/messages/message-encoder.d.ts +70 -0
  60. package/dist/messages/message-encoder.d.ts.map +1 -0
  61. package/dist/messages/message-encoder.js +453 -0
  62. package/dist/messages/message-encoder.js.map +1 -0
  63. package/dist/messages/send.d.ts +101 -0
  64. package/dist/messages/send.d.ts.map +1 -0
  65. package/dist/messages/send.js +409 -0
  66. package/dist/messages/send.js.map +1 -0
  67. package/dist/recovery/index.d.ts +125 -0
  68. package/dist/recovery/index.d.ts.map +1 -0
  69. package/dist/recovery/index.js +584 -0
  70. package/dist/recovery/index.js.map +1 -0
  71. package/dist/skdm/index.d.ts +220 -0
  72. package/dist/skdm/index.d.ts.map +1 -0
  73. package/dist/skdm/index.js +600 -0
  74. package/dist/skdm/index.js.map +1 -0
  75. package/dist/socket/hybrid.d.ts +118 -0
  76. package/dist/socket/hybrid.d.ts.map +1 -0
  77. package/dist/socket/hybrid.js +352 -0
  78. package/dist/socket/hybrid.js.map +1 -0
  79. package/dist/socket/wa-socket.d.ts +300 -0
  80. package/dist/socket/wa-socket.d.ts.map +1 -0
  81. package/dist/socket/wa-socket.js +1094 -0
  82. package/dist/socket/wa-socket.js.map +1 -0
  83. package/dist/socket/ws-socket.d.ts +96 -0
  84. package/dist/socket/ws-socket.d.ts.map +1 -0
  85. package/dist/socket/ws-socket.js +302 -0
  86. package/dist/socket/ws-socket.js.map +1 -0
  87. package/dist/types/index.d.ts +444 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +7 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/utils/jid.d.ts +46 -0
  92. package/dist/utils/jid.d.ts.map +1 -0
  93. package/dist/utils/jid.js +152 -0
  94. package/dist/utils/jid.js.map +1 -0
  95. package/dist/utils/logger.d.ts +45 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +79 -0
  98. package/dist/utils/logger.js.map +1 -0
  99. package/dist/utils/retry.d.ts +43 -0
  100. package/dist/utils/retry.d.ts.map +1 -0
  101. package/dist/utils/retry.js +175 -0
  102. package/dist/utils/retry.js.map +1 -0
  103. package/docs/API.md +745 -0
  104. package/docs/ARCHITECTURE.md +307 -0
  105. package/docs/BAILEYS_FIXES.md +360 -0
  106. package/docs/FEATURES.md +532 -0
  107. package/docs/PROTOCOL.md +489 -0
  108. package/docs/RECOVERY.md +409 -0
  109. package/docs/SKDM.md +233 -0
  110. package/examples/ai-bot/index.ts +479 -0
  111. package/examples/ai-bot/package.json +17 -0
  112. package/examples/echo-bot/index.ts +171 -0
  113. package/examples/echo-bot/package.json +18 -0
  114. package/examples/group-bot/index.ts +557 -0
  115. package/examples/group-bot/package.json +17 -0
  116. package/examples/rest-gateway/index.ts +499 -0
  117. package/examples/rest-gateway/package.json +19 -0
  118. package/package.json +75 -0
  119. package/src/advanced/features.ts +817 -0
  120. package/src/auth/auth-state.ts +342 -0
  121. package/src/auth/pairing-code.ts +246 -0
  122. package/src/auth/qr-code.ts +191 -0
  123. package/src/calls/handler.ts +153 -0
  124. package/src/core/binary.ts +464 -0
  125. package/src/core/crypto.ts +189 -0
  126. package/src/core/noise.ts +406 -0
  127. package/src/events/emitter.ts +88 -0
  128. package/src/features/index.ts +921 -0
  129. package/src/fixes/index.ts +882 -0
  130. package/src/groups/management.ts +497 -0
  131. package/src/index.ts +274 -0
  132. package/src/messages/media.ts +372 -0
  133. package/src/messages/message-encoder.ts +520 -0
  134. package/src/messages/send.ts +521 -0
  135. package/src/recovery/index.ts +704 -0
  136. package/src/skdm/index.ts +693 -0
  137. package/src/socket/hybrid.ts +414 -0
  138. package/src/socket/wa-socket.ts +1347 -0
  139. package/src/socket/ws-socket.ts +355 -0
  140. package/src/types/index.ts +457 -0
  141. package/src/utils/jid.ts +156 -0
  142. package/src/utils/logger.ts +84 -0
  143. package/src/utils/retry.ts +205 -0
  144. package/tsconfig.json +30 -0
@@ -0,0 +1,817 @@
1
+ /**
2
+ * Waliwa - Advanced features
3
+ * Polls, Newsletter, Status/Stories, Business Profile, Labels, Catalog
4
+ */
5
+
6
+ import type {
7
+ JIDLike,
8
+ WALogger,
9
+ WAMessage,
10
+ WAMessageKey,
11
+ PollUpdate
12
+ } from '../types/index.js';
13
+ import { encodeBinaryNode, BinaryNode } from '../core/binary.js';
14
+ import { jidToString } from '../utils/jid.js';
15
+ import { generateStanzaId, generateMessageId } from '../messages/message-encoder.js';
16
+ import { SilentLogger } from '../utils/logger.js';
17
+
18
+ // ============== Poll Manager ==============
19
+
20
+ export interface PollOptions {
21
+ name: string;
22
+ values: string[];
23
+ /** Max selectable options per user (default: 1 = single choice) */
24
+ selectableCounts?: number[];
25
+ /** Allow multiple answers */
26
+ multipleAnswers?: boolean;
27
+ }
28
+
29
+ export interface PollVote {
30
+ /** JID of voter */
31
+ voter: JIDLike;
32
+ /** Selected option indices */
33
+ selectedOptions: number[];
34
+ /** Timestamp */
35
+ timestamp: number;
36
+ }
37
+
38
+ export class PollManager {
39
+ private send: (data: Uint8Array) => void;
40
+ private registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
41
+ private logger: WALogger;
42
+ private voteListeners: Map<string, (vote: PollVote) => void> = new Map();
43
+
44
+ constructor(opts: {
45
+ send: (data: Uint8Array) => void;
46
+ registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
47
+ logger?: WALogger;
48
+ }) {
49
+ this.send = opts.send;
50
+ this.registerIqHandler = opts.registerIqHandler;
51
+ this.logger = opts.logger ?? new SilentLogger();
52
+ }
53
+
54
+ /**
55
+ * Create poll message
56
+ */
57
+ async createPoll(jid: JIDLike, opts: PollOptions): Promise<WAMessageKey> {
58
+ const messageId = generateMessageId();
59
+ const selectableCounts = opts.selectableCounts ?? [opts.multipleAnswers ? opts.values.length : 1];
60
+
61
+ const pollNode: BinaryNode = {
62
+ tag: 'message',
63
+ attrs: {
64
+ to: jidToString(jid),
65
+ type: 'poll',
66
+ id: messageId
67
+ },
68
+ content: [
69
+ {
70
+ tag: 'pollCreationMessage',
71
+ attrs: {
72
+ name: opts.name,
73
+ selectableOptionsCount: JSON.stringify(selectableCounts)
74
+ },
75
+ content: opts.values.map((value, idx) => ({
76
+ tag: 'option',
77
+ attrs: {
78
+ index: idx.toString(),
79
+ name: value
80
+ }
81
+ }))
82
+ }
83
+ ]
84
+ };
85
+
86
+ const data = encodeBinaryNode(pollNode);
87
+ this.send(data);
88
+ return {
89
+ remoteJid: jid,
90
+ fromMe: true,
91
+ id: messageId
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Vote on existing poll
97
+ */
98
+ async voteOnPoll(
99
+ pollKey: WAMessageKey,
100
+ selectedOptions: number[]
101
+ ): Promise<void> {
102
+ const messageId = generateMessageId();
103
+ const voteNode: BinaryNode = {
104
+ tag: 'message',
105
+ attrs: {
106
+ to: jidToString(pollKey.remoteJid),
107
+ type: 'poll_vote',
108
+ id: messageId
109
+ },
110
+ content: [
111
+ {
112
+ tag: 'pollVoteMessage',
113
+ attrs: {
114
+ pollCreationMessageKey: JSON.stringify({
115
+ remoteJid: jidToString(pollKey.remoteJid),
116
+ fromMe: pollKey.fromMe,
117
+ id: pollKey.id
118
+ })
119
+ },
120
+ content: selectedOptions.map(idx => ({
121
+ tag: 'selectedOption',
122
+ attrs: { index: idx.toString() }
123
+ }))
124
+ }
125
+ ]
126
+ };
127
+ this.send(encodeBinaryNode(voteNode));
128
+ }
129
+
130
+ /**
131
+ * Listen for poll votes
132
+ */
133
+ onVote(pollMessageId: string, handler: (vote: PollVote) => void): void {
134
+ this.voteListeners.set(pollMessageId, handler);
135
+ }
136
+
137
+ /**
138
+ * Handle incoming poll vote
139
+ */
140
+ handlePollVote(node: BinaryNode): void {
141
+ if (node.tag !== 'message') return;
142
+ const attrs = node.attrs || {};
143
+ const from = attrs.from;
144
+
145
+ if (Array.isArray(node.content)) {
146
+ for (const child of node.content) {
147
+ if (child?.tag === 'pollVoteMessage') {
148
+ const pollKeyJson = child.attrs?.pollCreationMessageKey;
149
+ if (!pollKeyJson) continue;
150
+ try {
151
+ const pollKey = JSON.parse(pollKeyJson);
152
+ const listener = this.voteListeners.get(pollKey.id);
153
+ if (!listener) continue;
154
+
155
+ const selectedOptions = Array.isArray(child.content)
156
+ ? child.content.filter(c => c?.tag === 'selectedOption').map(c => parseInt(c.attrs?.index ?? '0', 10))
157
+ : [];
158
+ listener({
159
+ voter: from,
160
+ selectedOptions,
161
+ timestamp: Date.now()
162
+ });
163
+ } catch (err: any) {
164
+ this.logger.warn(`Failed to parse poll vote: ${err.message}`);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // ============== Newsletter Manager ==============
173
+
174
+ export interface NewsletterMetadata {
175
+ id: JIDLike;
176
+ name: string;
177
+ description?: string;
178
+ subscribersCount: number;
179
+ state: 'active' | 'inactive';
180
+ createdAt?: number;
181
+ verification: 'verified' | 'unverified';
182
+ }
183
+
184
+ export class NewsletterManager {
185
+ private send: (data: Uint8Array) => void;
186
+ private registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
187
+ private logger: WALogger;
188
+
189
+ constructor(opts: {
190
+ send: (data: Uint8Array) => void;
191
+ registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
192
+ logger?: WALogger;
193
+ }) {
194
+ this.send = opts.send;
195
+ this.registerIqHandler = opts.registerIqHandler;
196
+ this.logger = opts.logger ?? new SilentLogger();
197
+ }
198
+
199
+ /**
200
+ * Create new newsletter
201
+ */
202
+ async create(name: string, description?: string): Promise<NewsletterMetadata> {
203
+ const id = generateStanzaId();
204
+ const node: BinaryNode = {
205
+ tag: 'iq',
206
+ attrs: {
207
+ type: 'set',
208
+ to: 's.whatsapp.net',
209
+ id
210
+ },
211
+ content: [
212
+ {
213
+ tag: 'newsletter',
214
+ attrs: {
215
+ name,
216
+ description: description ?? ''
217
+ }
218
+ }
219
+ ]
220
+ };
221
+ const response = await this.sendIqAndWait(node, id);
222
+ const nlNode = (response.content as any[])?.find(c => c.tag === 'newsletter');
223
+ return {
224
+ id: nlNode?.attrs?.id,
225
+ name,
226
+ description,
227
+ subscribersCount: 0,
228
+ state: 'active',
229
+ verification: 'unverified'
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Subscribe ke newsletter
235
+ */
236
+ async subscribe(newsletterId: JIDLike): Promise<void> {
237
+ const id = generateStanzaId();
238
+ const node: BinaryNode = {
239
+ tag: 'iq',
240
+ attrs: {
241
+ type: 'set',
242
+ to: jidToString(newsletterId),
243
+ id
244
+ },
245
+ content: [{
246
+ tag: 'subscribe',
247
+ attrs: { action: 'subscribe' }
248
+ }]
249
+ };
250
+ await this.sendIqAndWait(node, id);
251
+ }
252
+
253
+ /**
254
+ * Unsubscribe dari newsletter
255
+ */
256
+ async unsubscribe(newsletterId: JIDLike): Promise<void> {
257
+ const id = generateStanzaId();
258
+ const node: BinaryNode = {
259
+ tag: 'iq',
260
+ attrs: {
261
+ type: 'set',
262
+ to: jidToString(newsletterId),
263
+ id
264
+ },
265
+ content: [{
266
+ tag: 'subscribe',
267
+ attrs: { action: 'unsubscribe' }
268
+ }]
269
+ };
270
+ await this.sendIqAndWait(node, id);
271
+ }
272
+
273
+ /**
274
+ * Mute newsletter
275
+ */
276
+ async mute(newsletterId: JIDLike): Promise<void> {
277
+ const id = generateStanzaId();
278
+ const node: BinaryNode = {
279
+ tag: 'iq',
280
+ attrs: {
281
+ type: 'set',
282
+ to: jidToString(newsletterId),
283
+ id
284
+ },
285
+ content: [{
286
+ tag: 'mute',
287
+ attrs: { action: 'mute' }
288
+ }]
289
+ };
290
+ await this.sendIqAndWait(node, id);
291
+ }
292
+
293
+ private sendIqAndWait(node: BinaryNode, id: string, timeoutMs: number = 15000): Promise<BinaryNode> {
294
+ return new Promise((resolve, reject) => {
295
+ const timeout = setTimeout(() => reject(new Error(`IQ ${id} timeout`)), timeoutMs);
296
+ this.registerIqHandler(id, (response) => {
297
+ clearTimeout(timeout);
298
+ resolve(response);
299
+ });
300
+ this.send(encodeBinaryNode(node));
301
+ });
302
+ }
303
+ }
304
+
305
+ // ============== Status / Stories Manager ==============
306
+
307
+ export interface StatusOptions {
308
+ /** Background color for text-only status */
309
+ backgroundColor?: string;
310
+ /** Font for text status */
311
+ font?: number;
312
+ /** Caption */
313
+ caption?: string;
314
+ /** Status expiration in seconds (default 86400 = 24h) */
315
+ expiration?: number;
316
+ }
317
+
318
+ export class StatusManager {
319
+ private send: (data: Uint8Array) => void;
320
+ private logger: WALogger;
321
+
322
+ constructor(opts: { send: (data: Uint8Array) => void; logger?: WALogger }) {
323
+ this.send = opts.send;
324
+ this.logger = opts.logger ?? new SilentLogger();
325
+ }
326
+
327
+ /**
328
+ * Upload text status
329
+ */
330
+ async sendTextStatus(text: string, opts: StatusOptions = {}): Promise<WAMessageKey> {
331
+ const messageId = generateMessageId();
332
+ const node: BinaryNode = {
333
+ tag: 'message',
334
+ attrs: {
335
+ to: 'status@broadcast',
336
+ type: 'text',
337
+ id: messageId
338
+ },
339
+ content: [
340
+ {
341
+ tag: 'textStatusMessage',
342
+ attrs: {
343
+ text,
344
+ backgroundColor: opts.backgroundColor ?? '#000000',
345
+ font: (opts.font ?? 0).toString(),
346
+ expiration: ((opts.expiration ?? 86400) * 1000).toString()
347
+ }
348
+ }
349
+ ]
350
+ };
351
+ this.send(encodeBinaryNode(node));
352
+ return {
353
+ remoteJid: 'status@broadcast',
354
+ fromMe: true,
355
+ id: messageId
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Upload media status
361
+ */
362
+ async sendMediaStatus(
363
+ mediaType: 'image' | 'video',
364
+ mediaUrl: string,
365
+ opts: StatusOptions = {}
366
+ ): Promise<WAMessageKey> {
367
+ const messageId = generateMessageId();
368
+ const tag = mediaType === 'image' ? 'imageStatusMessage' : 'videoStatusMessage';
369
+ const node: BinaryNode = {
370
+ tag: 'message',
371
+ attrs: {
372
+ to: 'status@broadcast',
373
+ type: mediaType,
374
+ id: messageId
375
+ },
376
+ content: [
377
+ {
378
+ tag,
379
+ attrs: {
380
+ url: mediaUrl,
381
+ caption: opts.caption ?? '',
382
+ expiration: ((opts.expiration ?? 86400) * 1000).toString()
383
+ }
384
+ }
385
+ ]
386
+ };
387
+ this.send(encodeBinaryNode(node));
388
+ return {
389
+ remoteJid: 'status@broadcast',
390
+ fromMe: true,
391
+ id: messageId
392
+ };
393
+ }
394
+
395
+ /**
396
+ * Delete own status
397
+ */
398
+ async deleteStatus(statusId: string): Promise<void> {
399
+ const node: BinaryNode = {
400
+ tag: 'iq',
401
+ attrs: {
402
+ type: 'set',
403
+ to: 'status@broadcast',
404
+ id: `delstatus-${Date.now()}`
405
+ },
406
+ content: [{
407
+ tag: 'delete',
408
+ attrs: { id: statusId }
409
+ }]
410
+ };
411
+ this.send(encodeBinaryNode(node));
412
+ }
413
+
414
+ /**
415
+ * Get status views
416
+ */
417
+ async getStatusViews(statusId: string): Promise<JIDLike[]> {
418
+ const id = generateStanzaId();
419
+ const node: BinaryNode = {
420
+ tag: 'iq',
421
+ attrs: {
422
+ type: 'get',
423
+ to: 'status@broadcast',
424
+ id
425
+ },
426
+ content: [{
427
+ tag: 'views',
428
+ attrs: { id: statusId }
429
+ }]
430
+ };
431
+ // Send and wait for response (simplified - real impl uses IQ handler registry)
432
+ this.send(encodeBinaryNode(node));
433
+ return [];
434
+ }
435
+ }
436
+
437
+ // ============== Business Profile Manager ==============
438
+
439
+ export interface BusinessProfileOptions {
440
+ description?: string;
441
+ email?: string;
442
+ websites?: string[];
443
+ category?: string;
444
+ address?: string;
445
+ businessHours?: BusinessHours[];
446
+ }
447
+
448
+ export interface BusinessHours {
449
+ day: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
450
+ openTime: string; // "09:00"
451
+ closeTime: string; // "17:00"
452
+ mode: 'open' | 'closed' | 'by_appointment';
453
+ }
454
+
455
+ export class BusinessProfileManager {
456
+ private send: (data: Uint8Array) => void;
457
+ private registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
458
+ private logger: WALogger;
459
+
460
+ constructor(opts: {
461
+ send: (data: Uint8Array) => void;
462
+ registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
463
+ logger?: WALogger;
464
+ }) {
465
+ this.send = opts.send;
466
+ this.registerIqHandler = opts.registerIqHandler;
467
+ this.logger = opts.logger ?? new SilentLogger();
468
+ }
469
+
470
+ /**
471
+ * Update business profile
472
+ */
473
+ async update(opts: BusinessProfileOptions): Promise<void> {
474
+ const id = generateStanzaId();
475
+ const content: any[] = [];
476
+ if (opts.description) {
477
+ content.push({ tag: 'description', attrs: {}, content: opts.description });
478
+ }
479
+ if (opts.email) {
480
+ content.push({ tag: 'email', attrs: {}, content: opts.email });
481
+ }
482
+ if (opts.websites?.length) {
483
+ content.push({
484
+ tag: 'websites',
485
+ attrs: {},
486
+ content: opts.websites.map(w => ({ tag: 'website', attrs: { url: w } }))
487
+ });
488
+ }
489
+ if (opts.category) {
490
+ content.push({ tag: 'category', attrs: { name: opts.category } });
491
+ }
492
+ if (opts.address) {
493
+ content.push({ tag: 'address', attrs: {}, content: opts.address });
494
+ }
495
+ if (opts.businessHours?.length) {
496
+ content.push({
497
+ tag: 'business_hours',
498
+ attrs: {},
499
+ content: opts.businessHours.map(h => ({
500
+ tag: 'business_hours_config',
501
+ attrs: {
502
+ day_of_week: h.day,
503
+ mode: h.mode,
504
+ open_time: h.openTime,
505
+ close_time: h.closeTime
506
+ }
507
+ }))
508
+ });
509
+ }
510
+
511
+ const node: BinaryNode = {
512
+ tag: 'iq',
513
+ attrs: {
514
+ type: 'set',
515
+ to: 's.whatsapp.net',
516
+ id
517
+ },
518
+ content: [
519
+ {
520
+ tag: 'business_profile',
521
+ attrs: {},
522
+ content
523
+ }
524
+ ]
525
+ };
526
+ await this.sendIqAndWait(node, id);
527
+ }
528
+
529
+ /**
530
+ * Get business profile
531
+ */
532
+ async get(jid: JIDLike): Promise<BusinessProfileOptions | null> {
533
+ const id = generateStanzaId();
534
+ const node: BinaryNode = {
535
+ tag: 'iq',
536
+ attrs: {
537
+ type: 'get',
538
+ to: jidToString(jid),
539
+ id
540
+ },
541
+ content: [{
542
+ tag: 'business_profile',
543
+ attrs: {}
544
+ }]
545
+ };
546
+ const response = await this.sendIqAndWait(node, id);
547
+ const profileNode = (response.content as any[])?.find(c => c.tag === 'business_profile');
548
+ if (!profileNode) return null;
549
+
550
+ const result: BusinessProfileOptions = {};
551
+ if (Array.isArray(profileNode.content)) {
552
+ for (const child of profileNode.content) {
553
+ switch (child.tag) {
554
+ case 'description':
555
+ result.description = typeof child.content === 'string' ? child.content : '';
556
+ break;
557
+ case 'email':
558
+ result.email = typeof child.content === 'string' ? child.content : '';
559
+ break;
560
+ case 'category':
561
+ result.category = child.attrs?.name;
562
+ break;
563
+ case 'address':
564
+ result.address = typeof child.content === 'string' ? child.content : '';
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ return result;
570
+ }
571
+
572
+ private sendIqAndWait(node: BinaryNode, id: string, timeoutMs: number = 15000): Promise<BinaryNode> {
573
+ return new Promise((resolve, reject) => {
574
+ const timeout = setTimeout(() => reject(new Error(`IQ ${id} timeout`)), timeoutMs);
575
+ this.registerIqHandler(id, (response) => {
576
+ clearTimeout(timeout);
577
+ resolve(response);
578
+ });
579
+ this.send(encodeBinaryNode(node));
580
+ });
581
+ }
582
+ }
583
+
584
+ // ============== Catalog Manager ==============
585
+
586
+ export interface CatalogItem {
587
+ id: string;
588
+ name: string;
589
+ description?: string;
590
+ price: number;
591
+ currency: string;
592
+ url?: string;
593
+ imageUrl?: string;
594
+ }
595
+
596
+ export class CatalogManager {
597
+ private send: (data: Uint8Array) => void;
598
+ private registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
599
+ private logger: WALogger;
600
+
601
+ constructor(opts: {
602
+ send: (data: Uint8Array) => void;
603
+ registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
604
+ logger?: WALogger;
605
+ }) {
606
+ this.send = opts.send;
607
+ this.registerIqHandler = opts.registerIqHandler;
608
+ this.logger = opts.logger ?? new SilentLogger();
609
+ }
610
+
611
+ /**
612
+ * Add product to catalog
613
+ */
614
+ async addProduct(item: Omit<CatalogItem, 'id'>): Promise<string> {
615
+ const id = generateStanzaId();
616
+ const productId = `prod-${Date.now()}`;
617
+ const node: BinaryNode = {
618
+ tag: 'iq',
619
+ attrs: {
620
+ type: 'set',
621
+ to: 's.whatsapp.net',
622
+ id
623
+ },
624
+ content: [{
625
+ tag: 'product',
626
+ attrs: {
627
+ id: productId,
628
+ name: item.name,
629
+ price: item.price.toString(),
630
+ currency: item.currency
631
+ },
632
+ content: [
633
+ item.description ? { tag: 'description', attrs: {}, content: item.description } : null,
634
+ item.url ? { tag: 'url', attrs: {}, content: item.url } : null
635
+ ].filter(Boolean) as any[]
636
+ }]
637
+ };
638
+ await this.sendIqAndWait(node, id);
639
+ return productId;
640
+ }
641
+
642
+ /**
643
+ * Get catalog for business account
644
+ */
645
+ async getCatalog(jid: JIDLike): Promise<CatalogItem[]> {
646
+ const id = generateStanzaId();
647
+ const node: BinaryNode = {
648
+ tag: 'iq',
649
+ attrs: {
650
+ type: 'get',
651
+ to: jidToString(jid),
652
+ id
653
+ },
654
+ content: [{
655
+ tag: 'catalog',
656
+ attrs: {}
657
+ }]
658
+ };
659
+ const response = await this.sendIqAndWait(node, id);
660
+ const catalogNode = (response.content as any[])?.find(c => c.tag === 'catalog');
661
+ if (!catalogNode) return [];
662
+
663
+ const items: CatalogItem[] = [];
664
+ if (Array.isArray(catalogNode.content)) {
665
+ for (const child of catalogNode.content) {
666
+ if (child.tag === 'product') {
667
+ items.push({
668
+ id: child.attrs?.id,
669
+ name: child.attrs?.name,
670
+ description: typeof child.content === 'string' ? child.content : '',
671
+ price: parseFloat(child.attrs?.price ?? '0'),
672
+ currency: child.attrs?.currency ?? 'USD',
673
+ url: child.attrs?.url,
674
+ imageUrl: child.attrs?.imageUrl
675
+ });
676
+ }
677
+ }
678
+ }
679
+ return items;
680
+ }
681
+
682
+ private sendIqAndWait(node: BinaryNode, id: string, timeoutMs: number = 15000): Promise<BinaryNode> {
683
+ return new Promise((resolve, reject) => {
684
+ const timeout = setTimeout(() => reject(new Error(`IQ ${id} timeout`)), timeoutMs);
685
+ this.registerIqHandler(id, (response) => {
686
+ clearTimeout(timeout);
687
+ resolve(response);
688
+ });
689
+ this.send(encodeBinaryNode(node));
690
+ });
691
+ }
692
+ }
693
+
694
+ // ============== Labels Manager ==============
695
+
696
+ export interface Label {
697
+ id: string;
698
+ name: string;
699
+ color: number;
700
+ priority?: number;
701
+ deleted?: boolean;
702
+ }
703
+
704
+ export class LabelsManager {
705
+ private send: (data: Uint8Array) => void;
706
+ private registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
707
+ private logger: WALogger;
708
+
709
+ constructor(opts: {
710
+ send: (data: Uint8Array) => void;
711
+ registerIqHandler: (id: string, handler: (node: BinaryNode) => void) => void;
712
+ logger?: WALogger;
713
+ }) {
714
+ this.send = opts.send;
715
+ this.registerIqHandler = opts.registerIqHandler;
716
+ this.logger = opts.logger ?? new SilentLogger();
717
+ }
718
+
719
+ /**
720
+ * Create label
721
+ */
722
+ async create(name: string, color: number): Promise<Label> {
723
+ const id = generateStanzaId();
724
+ const labelId = `label-${Date.now()}`;
725
+ const node: BinaryNode = {
726
+ tag: 'iq',
727
+ attrs: {
728
+ type: 'set',
729
+ to: 's.whatsapp.net',
730
+ id
731
+ },
732
+ content: [{
733
+ tag: 'label',
734
+ attrs: {
735
+ id: labelId,
736
+ name,
737
+ color: color.toString()
738
+ }
739
+ }]
740
+ };
741
+ await this.sendIqAndWait(node, id);
742
+ return { id: labelId, name, color };
743
+ }
744
+
745
+ /**
746
+ * Assign label ke chat
747
+ */
748
+ async assignToChat(labelId: string, chatJid: JIDLike): Promise<void> {
749
+ const id = generateStanzaId();
750
+ const node: BinaryNode = {
751
+ tag: 'iq',
752
+ attrs: {
753
+ type: 'set',
754
+ to: 's.whatsapp.net',
755
+ id
756
+ },
757
+ content: [{
758
+ tag: 'label',
759
+ attrs: {
760
+ id: labelId,
761
+ action: 'assign',
762
+ chat: jidToString(chatJid)
763
+ }
764
+ }]
765
+ };
766
+ await this.sendIqAndWait(node, id);
767
+ }
768
+
769
+ /**
770
+ * Get all labels
771
+ */
772
+ async list(): Promise<Label[]> {
773
+ const id = generateStanzaId();
774
+ const node: BinaryNode = {
775
+ tag: 'iq',
776
+ attrs: {
777
+ type: 'get',
778
+ to: 's.whatsapp.net',
779
+ id
780
+ },
781
+ content: [{
782
+ tag: 'labels',
783
+ attrs: {}
784
+ }]
785
+ };
786
+ const response = await this.sendIqAndWait(node, id);
787
+ const labelsNode = (response.content as any[])?.find(c => c.tag === 'labels');
788
+ if (!labelsNode) return [];
789
+
790
+ const labels: Label[] = [];
791
+ if (Array.isArray(labelsNode.content)) {
792
+ for (const child of labelsNode.content) {
793
+ if (child.tag === 'label') {
794
+ labels.push({
795
+ id: child.attrs?.id,
796
+ name: child.attrs?.name,
797
+ color: parseInt(child.attrs?.color ?? '0', 10),
798
+ priority: child.attrs?.priority ? parseInt(child.attrs.priority, 10) : undefined,
799
+ deleted: child.attrs?.deleted === 'true'
800
+ });
801
+ }
802
+ }
803
+ }
804
+ return labels;
805
+ }
806
+
807
+ private sendIqAndWait(node: BinaryNode, id: string, timeoutMs: number = 15000): Promise<BinaryNode> {
808
+ return new Promise((resolve, reject) => {
809
+ const timeout = setTimeout(() => reject(new Error(`IQ ${id} timeout`)), timeoutMs);
810
+ this.registerIqHandler(id, (response) => {
811
+ clearTimeout(timeout);
812
+ resolve(response);
813
+ });
814
+ this.send(encodeBinaryNode(node));
815
+ });
816
+ }
817
+ }