@areumtecnologia/wasockets 2.2.3 → 2.3.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 (31) hide show
  1. package/README.md +74 -16
  2. package/lib/Client/Builders/Buttons.js +127 -0
  3. package/lib/Client/Builders/EventBuilder.js +122 -0
  4. package/lib/Client/Builders/InteractiveMessage.js +150 -0
  5. package/lib/Client/Builders/Lists.js +78 -0
  6. package/lib/Client/Builders/PollBuilder.js +84 -0
  7. package/lib/Client/Builders/index.js +21 -0
  8. package/lib/Client/Constants.js +54 -0
  9. package/lib/Client/Context/CallContext.js +96 -0
  10. package/lib/Client/Context/MediaObject.js +155 -0
  11. package/lib/Client/Context/MessageContext.js +146 -0
  12. package/lib/Client/Modules/BusinessManager.js +155 -0
  13. package/lib/Client/Modules/ChannelManager.js +144 -0
  14. package/lib/Client/Modules/ChatManager.js +113 -0
  15. package/lib/Client/Modules/CommunityManager.js +136 -0
  16. package/lib/Client/Modules/GroupManager.js +194 -0
  17. package/lib/Client/Modules/LabelManager.js +123 -0
  18. package/lib/Client/Modules/MessageManager.js +198 -0
  19. package/lib/Client/Modules/PresenceManager.js +68 -0
  20. package/lib/Client/Modules/PrivacyManager.js +98 -0
  21. package/lib/Client/Modules/ProfileManager.js +92 -0
  22. package/lib/Client/Modules/UserManager.js +92 -0
  23. package/lib/Client/Modules/VoipManager.js +117 -0
  24. package/lib/Client/Modules/index.js +29 -0
  25. package/lib/Client/Normalizers/EventNormalizer.js +82 -0
  26. package/lib/Client/Normalizers/JidNormalizer.js +93 -0
  27. package/lib/Client/Normalizers/MessageNormalizer.js +134 -0
  28. package/lib/Client/WaClient.js +438 -0
  29. package/lib/Client/index.d.ts +464 -0
  30. package/lib/Client/index.js +76 -0
  31. package/package.json +29 -23
@@ -0,0 +1,438 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const EventEmitter = require("events")
6
+ const { join } = require("path")
7
+ const { default: makeWASocket } = require("../Socket")
8
+ const { useMultiFileAuthState } = require("../Utils/use-multi-file-auth-state")
9
+ const { shouldReconnect, getReconnectDelay } = require("../Utils/reconnect")
10
+ const { Events, ConnectionStatus } = require("./Constants")
11
+ const { JidNormalizer } = require("./Normalizers/JidNormalizer")
12
+ const { EventNormalizer } = require("./Normalizers/EventNormalizer")
13
+ const {
14
+ MessageManager,
15
+ GroupManager,
16
+ ChannelManager,
17
+ PresenceManager,
18
+ ProfileManager,
19
+ PrivacyManager,
20
+ UserManager,
21
+ ChatManager,
22
+ VoipManager,
23
+ BusinessManager,
24
+ CommunityManager,
25
+ LabelManager
26
+ } = require("./Modules")
27
+
28
+ /**
29
+ * WaClient - High-Level Unified WhatsApp Client
30
+ */
31
+ class WaClient extends EventEmitter {
32
+ /**
33
+ * @param {Object} [options={}]
34
+ * @param {string} [options.sessionName='default']
35
+ * @param {string} [options.dataPath='./sessions']
36
+ * @param {Object} [options.auth] - Optional custom auth { state, saveCreds }
37
+ * @param {string} [options.phoneNumber] - Phone number to authenticate via Pairing Code
38
+ * @param {boolean} [options.printQRInTerminal=true]
39
+ * @param {boolean} [options.restartOnClose=true]
40
+ * @param {number} [options.maxReconnectAttempts=15]
41
+ * @param {boolean} [options.syncFullHistory=false]
42
+ * @param {boolean} [options.markOnlineOnConnect=true]
43
+ * @param {Object} [options.socketOptions={}] - Raw socket configuration overrides
44
+ * @param {any} [options.logger]
45
+ */
46
+ constructor(options = {}) {
47
+ super()
48
+
49
+ this.options = {
50
+ sessionName: 'default',
51
+ dataPath: './sessions',
52
+ printQRInTerminal: true,
53
+ restartOnClose: true,
54
+ maxReconnectAttempts: 15,
55
+ syncFullHistory: false,
56
+ markOnlineOnConnect: true,
57
+ socketOptions: {},
58
+ ...options
59
+ }
60
+
61
+ this.status = ConnectionStatus.IDLE
62
+ this.sock = null
63
+ this.authState = null
64
+ this.saveCreds = null
65
+ this.reconnectAttempts = 0
66
+ this._reconnectTimer = null
67
+ this._isDestroyed = false
68
+
69
+ // Specialized Domain Managers
70
+ this.messages = new MessageManager(this)
71
+ this.groups = new GroupManager(this)
72
+ this.channels = new ChannelManager(this)
73
+ this.presence = new PresenceManager(this)
74
+ this.profile = new ProfileManager(this)
75
+ this.privacy = new PrivacyManager(this)
76
+ this.users = new UserManager(this)
77
+ this.chat = new ChatManager(this)
78
+ this.voip = new VoipManager(this)
79
+ this.business = new BusinessManager(this)
80
+ this.communities = new CommunityManager(this)
81
+ this.labels = new LabelManager(this)
82
+ }
83
+
84
+ /**
85
+ * Connects to WhatsApp
86
+ * @returns {Promise<this>}
87
+ */
88
+ async connect() {
89
+ if (this._isDestroyed) {
90
+ throw new Error('WaClient instance was destroyed. Create a new instance.')
91
+ }
92
+
93
+ this._clearReconnectTimer()
94
+ this._setStatus(ConnectionStatus.INITIALIZING)
95
+
96
+ try {
97
+ // 1. Initialize or load auth state
98
+ if (this.options.auth?.state && this.options.auth?.saveCreds) {
99
+ this.authState = this.options.auth.state
100
+ this.saveCreds = this.options.auth.saveCreds
101
+ } else {
102
+ const sessionFolder = join(this.options.dataPath, this.options.sessionName)
103
+ const { state, saveCreds } = await useMultiFileAuthState(sessionFolder)
104
+ this.authState = state
105
+ this.saveCreds = saveCreds
106
+ }
107
+
108
+ this._setStatus(ConnectionStatus.CONNECTING)
109
+
110
+ // 2. Configure and create underlying socket
111
+ const hasPhoneNumber = Boolean(this.options.phoneNumber)
112
+ const socketConfig = {
113
+ auth: this.authState,
114
+ printQRInTerminal: hasPhoneNumber ? false : this.options.printQRInTerminal,
115
+ syncFullHistory: this.options.syncFullHistory,
116
+ markOnlineOnConnect: this.options.markOnlineOnConnect,
117
+ logger: this.options.logger,
118
+ ...this.options.socketOptions
119
+ }
120
+
121
+ this.sock = makeWASocket(socketConfig)
122
+
123
+ // 3. Set up credentials persistence
124
+ this.sock.ev.on('creds.update', async () => {
125
+ if (typeof this.saveCreds === 'function') {
126
+ try {
127
+ await this.saveCreds()
128
+ } catch (err) {
129
+ this.emit(Events.ERROR, err)
130
+ }
131
+ }
132
+ })
133
+
134
+ // 4. Set up connection lifecycle
135
+ this._setupConnectionLifecycle()
136
+
137
+ // 5. Bind normalized events (messages, contacts, presence)
138
+ EventNormalizer.bindSocketEvents(this)
139
+
140
+ // 6. Handle pairing code if configured
141
+ if (hasPhoneNumber && !this.sock.authState.creds.registered) {
142
+ this._handlePairingCode(this.options.phoneNumber)
143
+ }
144
+
145
+ return this
146
+ } catch (error) {
147
+ this._setStatus(ConnectionStatus.DISCONNECTED)
148
+ this.emit(Events.ERROR, error)
149
+ throw error
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Sets up low-level connection listeners
155
+ * @private
156
+ */
157
+ _setupConnectionLifecycle() {
158
+ this.sock.ev.on('connection.update', async (update) => {
159
+ const { connection, lastDisconnect, qr } = update
160
+
161
+ // QR Code issued
162
+ if (qr) {
163
+ this._setStatus(ConnectionStatus.QR_READY)
164
+ this.emit(Events.QR_CODE, { qr })
165
+ }
166
+
167
+ // Connection successfully established
168
+ if (connection === 'open') {
169
+ this.reconnectAttempts = 0
170
+ this._setStatus(ConnectionStatus.CONNECTED)
171
+ this.emit(Events.CONNECTED, { user: this.sock.user })
172
+ }
173
+
174
+ // Connection closed or dropped
175
+ if (connection === 'close') {
176
+ const error = lastDisconnect?.error
177
+ const canReconnect = shouldReconnect(error)
178
+
179
+ if (!canReconnect) {
180
+ this._setStatus(ConnectionStatus.LOGGED_OUT)
181
+ this.emit(Events.LOGGED_OUT, { error })
182
+ } else if (this.options.restartOnClose && !this._isDestroyed) {
183
+ this._scheduleReconnect(error)
184
+ } else {
185
+ this._setStatus(ConnectionStatus.DISCONNECTED)
186
+ this.emit(Events.DISCONNECTED, { error, willReconnect: false })
187
+ }
188
+ }
189
+ })
190
+ }
191
+
192
+ /**
193
+ * Handles pairing code requests
194
+ * @private
195
+ */
196
+ _handlePairingCode(phoneNumber) {
197
+ const cleanNumber = JidNormalizer.toPhoneNumber(phoneNumber)
198
+ if (!cleanNumber) return
199
+
200
+ const attemptPairing = async (retryCount = 0) => {
201
+ if (this._isDestroyed || !this.sock || this.sock.authState.creds.registered) return
202
+
203
+ try {
204
+ this._setStatus(ConnectionStatus.PAIRING_READY)
205
+ const code = await this.sock.requestPairingCode(cleanNumber)
206
+ this.emit(Events.PAIRING_CODE, { code })
207
+ } catch (err) {
208
+ if (retryCount < 3 && !this._isDestroyed) {
209
+ setTimeout(() => attemptPairing(retryCount + 1), 3000)
210
+ } else {
211
+ this.emit(Events.ERROR, err)
212
+ }
213
+ }
214
+ }
215
+
216
+ // Slight delay to ensure socket handshake allows pairing node
217
+ setTimeout(() => attemptPairing(0), 2000)
218
+ }
219
+
220
+ /**
221
+ * Schedules reconnection with exponential backoff and jitter
222
+ * @private
223
+ */
224
+ _scheduleReconnect(error) {
225
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
226
+ this._setStatus(ConnectionStatus.DISCONNECTED)
227
+ this.emit(Events.DISCONNECTED, {
228
+ error,
229
+ willReconnect: false,
230
+ reason: 'Max reconnect attempts exceeded'
231
+ })
232
+ return
233
+ }
234
+
235
+ this.reconnectAttempts++
236
+ const delayMs = getReconnectDelay(this.reconnectAttempts, {
237
+ statusCode: error?.output?.statusCode || error?.statusCode
238
+ })
239
+
240
+ this._setStatus(ConnectionStatus.RECONNECTING)
241
+ this.emit(Events.DISCONNECTED, {
242
+ error,
243
+ willReconnect: true,
244
+ attempt: this.reconnectAttempts,
245
+ delayMs
246
+ })
247
+
248
+ this._clearReconnectTimer()
249
+ this._reconnectTimer = setTimeout(() => {
250
+ if (!this._isDestroyed) {
251
+ this.connect().catch((err) => {
252
+ this.emit(Events.ERROR, err)
253
+ })
254
+ }
255
+ }, delayMs)
256
+ }
257
+
258
+ /**
259
+ * Updates internal status and emits STATUS_CHANGE event
260
+ * @private
261
+ */
262
+ _setStatus(newStatus) {
263
+ const previousStatus = this.status
264
+ if (previousStatus === newStatus) return
265
+
266
+ this.status = newStatus
267
+ this.emit(Events.STATUS_CHANGE, {
268
+ current: newStatus,
269
+ previous: previousStatus
270
+ })
271
+ }
272
+
273
+ _clearReconnectTimer() {
274
+ if (this._reconnectTimer) {
275
+ clearTimeout(this._reconnectTimer)
276
+ this._reconnectTimer = null
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Sends a message to a recipient
282
+ * @param {string} jid - Destination phone number or JID
283
+ * @param {Object} content - Message content object
284
+ * @param {Object} [options={}] - Additional options (quoted, etc.)
285
+ */
286
+ async sendMessage(jid, content, options = {}) {
287
+ if (!this.sock) {
288
+ throw new Error('Cannot send message: socket is not connected.')
289
+ }
290
+ const targetJid = JidNormalizer.toJid(jid)
291
+ return this.sock.sendMessage(targetJid, content, options)
292
+ }
293
+
294
+ /**
295
+ * Sends an interactive message, buttons, list or card
296
+ * @param {string} jid
297
+ * @param {Object|InteractiveMessage} interactiveOrBuilder
298
+ * @param {Object} [options={}]
299
+ */
300
+ async sendInteractive(jid, interactiveOrBuilder, options = {}) {
301
+ const payload = typeof interactiveOrBuilder?.build === 'function'
302
+ ? interactiveOrBuilder.build()
303
+ : interactiveOrBuilder
304
+ return this.sendMessage(jid, payload, options)
305
+ }
306
+
307
+ /**
308
+ * Sends a poll to a user or group
309
+ * @param {string} jid
310
+ * @param {Object|PollBuilder} pollOrBuilder
311
+ * @param {Object} [options={}]
312
+ */
313
+ async sendPoll(jid, pollOrBuilder, options = {}) {
314
+ const payload = typeof pollOrBuilder?.build === 'function'
315
+ ? pollOrBuilder.build()
316
+ : pollOrBuilder
317
+ return this.sendMessage(jid, payload, options)
318
+ }
319
+
320
+ /**
321
+ * Sends a group event invitation
322
+ * @param {string} jid
323
+ * @param {Object|EventBuilder} eventOrBuilder
324
+ * @param {Object} [options={}]
325
+ */
326
+ async sendEvent(jid, eventOrBuilder, options = {}) {
327
+ const payload = typeof eventOrBuilder?.build === 'function'
328
+ ? eventOrBuilder.build()
329
+ : eventOrBuilder
330
+ return this.sendMessage(jid, payload, options)
331
+ }
332
+
333
+ /**
334
+ * Starts typing (composing) state
335
+ * @param {string} jid
336
+ * @returns {{ stop: () => Promise<void> }}
337
+ */
338
+ startTyping(jid) {
339
+ const targetJid = JidNormalizer.toJid(jid)
340
+ if (this.sock?.sendPresenceUpdate) {
341
+ this.sock.sendPresenceUpdate('composing', targetJid).catch(() => {})
342
+ }
343
+ return {
344
+ stop: async () => {
345
+ if (this.sock?.sendPresenceUpdate) {
346
+ await this.sock.sendPresenceUpdate('paused', targetJid).catch(() => {})
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Starts voice recording state
354
+ * @param {string} jid
355
+ * @returns {{ stop: () => Promise<void> }}
356
+ */
357
+ startRecording(jid) {
358
+ const targetJid = JidNormalizer.toJid(jid)
359
+ if (this.sock?.sendPresenceUpdate) {
360
+ this.sock.sendPresenceUpdate('recording', targetJid).catch(() => {})
361
+ }
362
+ return {
363
+ stop: async () => {
364
+ if (this.sock?.sendPresenceUpdate) {
365
+ await this.sock.sendPresenceUpdate('paused', targetJid).catch(() => {})
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Verifies if a phone number or JID exists on WhatsApp
373
+ * @param {string} jidOrNumber
374
+ * @returns {Promise<{ exists: boolean, jid: string } | null>}
375
+ */
376
+ async isOnWhatsApp(jidOrNumber) {
377
+ if (!this.sock?.onWhatsApp) {
378
+ throw new Error('Socket is not connected.')
379
+ }
380
+ const cleanNumber = JidNormalizer.toPhoneNumber(jidOrNumber) || jidOrNumber
381
+ const results = await this.sock.onWhatsApp(cleanNumber)
382
+ if (Array.isArray(results) && results.length > 0) {
383
+ return {
384
+ exists: Boolean(results[0]?.exists),
385
+ jid: results[0]?.jid || ''
386
+ }
387
+ }
388
+ return null
389
+ }
390
+
391
+ /**
392
+ * Closes the connection gracefully without deleting credentials
393
+ */
394
+ async disconnect() {
395
+ this._isDestroyed = true
396
+ this._clearReconnectTimer()
397
+ this._setStatus(ConnectionStatus.DISCONNECTED)
398
+ if (this.sock) {
399
+ this.sock.end(undefined)
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Logs out from WhatsApp and revokes device credentials
405
+ */
406
+ async logout() {
407
+ this._isDestroyed = true
408
+ this._clearReconnectTimer()
409
+ this._setStatus(ConnectionStatus.LOGGED_OUT)
410
+ if (this.sock) {
411
+ await this.sock.logout('WaClient Intentional Logout')
412
+ }
413
+ }
414
+
415
+ /**
416
+ * The authenticated user profile or null
417
+ */
418
+ get user() {
419
+ return this.sock?.user || null
420
+ }
421
+
422
+ /**
423
+ * True if currently connected and open
424
+ */
425
+ get isConnected() {
426
+ return this.status === ConnectionStatus.CONNECTED
427
+ }
428
+
429
+ /**
430
+ * Direct access to underlying low-level socket
431
+ */
432
+ get rawSocket() {
433
+ return this.sock
434
+ }
435
+ }
436
+
437
+ exports.WaClient = WaClient
438
+ exports.default = WaClient