soblendwa-client 0.0.0-stage → 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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -2
  3. package/index.d.ts +255 -0
  4. package/index.js +444 -0
  5. package/package.json +33 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Soblend Development Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,98 @@
1
- # Temporary Holding Version
1
+ # soblendwa-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Cliente de JavaScript y TypeScript para el servidor de
4
+ [SoblendWA](https://github.com/Soblend-Development-Studio/SoblendWA). Hecho por **Soblend Development Studio**.
5
+
6
+ El motor (Rust) corre en `soblendwa-server`; tu bot solo habla con él por WebSocket. No compila nada nativo, no tiene
7
+ dependencias y funciona en Node 18+, Bun, Deno y el navegador. En Node 22+, Bun y Deno usa el WebSocket incluido; en
8
+ Node 18–21 instala `ws`.
9
+
10
+ ```bash
11
+ npm install soblendwa-client
12
+ ```
13
+
14
+ ## 1. Arranca el servidor
15
+
16
+ Descarga el binario de tu plataforma en
17
+ [Releases](https://github.com/Soblend-Development-Studio/SoblendWA/releases) y córrelo:
18
+
19
+ ```text
20
+ ./soblendwa-server --token mi-clave
21
+ ```
22
+
23
+ O, si tienes Rust y el repositorio: `cargo run --release -p soblendwa-server -- --token mi-clave`.
24
+
25
+ La primera vez muestra un QR en la terminal: escanéalo en **WhatsApp > Dispositivos vinculados**.
26
+
27
+ Para probar sin ninguna cuenta: `./soblendwa-server --fake --session bot --session ana` usa un WhatsApp falso con el
28
+ protocolo real, y nada sale de tu máquina.
29
+
30
+ ## 2. Escribe tu bot
31
+
32
+ ```js
33
+ import { connect } from 'soblendwa-client'
34
+
35
+ const bot = await connect('ws://127.0.0.1:8787', { token: 'mi-clave' })
36
+
37
+ bot.on('message', async (message) => {
38
+ if (message.info.fromMe) return
39
+ if (message.text === '!ping') {
40
+ const ms = await bot.elapsed(message)
41
+ await bot.reply(message, `Pong\n${ms.toFixed(2)} ms`)
42
+ }
43
+ if (message.text === '!menu') {
44
+ await bot.sendInteractive(message.info.chat, {
45
+ body: '¿Qué quieres hacer?',
46
+ buttons: [
47
+ { type: 'reply', id: 'ping', text: 'Medir latencia' },
48
+ { type: 'url', text: 'Rust', url: 'https://www.rust-lang.org' },
49
+ ],
50
+ })
51
+ }
52
+ if (message.reply?.id === 'ping') await bot.reply(message, 'Pong')
53
+ })
54
+ ```
55
+
56
+ Con TypeScript es igual: los tipos vienen incluidos.
57
+
58
+ Si te desconectas del servidor, el cliente se reconecta solo. WhatsApp sigue conectado en el servidor mientras tanto,
59
+ así que no se pierde la sesión.
60
+
61
+ ## Varias cuentas
62
+
63
+ Cada sesión es una cuenta de WhatsApp distinta y el servidor las maneja todas a la vez:
64
+
65
+ ```js
66
+ const ventas = await connect(url, { session: 'ventas', token })
67
+ const soporte = await connect(url, { session: 'soporte', token })
68
+ ```
69
+
70
+ ## Vista previa de enlaces
71
+
72
+ Los enlaces de un texto llevan vista previa sola. Para quitarla o escribirla a mano:
73
+
74
+ ```js
75
+ await bot.sendText(chat, texto, { preview: false })
76
+ await bot.sendText(chat, texto, { preview: { title: 'Mi título', description: 'Escrito a mano' } })
77
+ ```
78
+
79
+ ## Métodos
80
+
81
+ Los mismos que el módulo nativo `soblendwa`: `sendText`, `reply`, `react`, `read`, `sendImage`, `sendVideo`,
82
+ `sendAudio`, `sendDocument`, `sendSticker`, `sendFile`, `download`, `downloadToFile`, `sendPoll`, `votePoll`, `edit`,
83
+ `revoke`, `typing`, `setOnline`, `sendInteractive`, `sendButtons`, `sendList`, `sendContacts`, `sendMentionAll`,
84
+ `sendGroupInvite`, `setMemberLabel`, `sendProto`, `groupInfo`, `joinedGroups`, `onWhatsApp`, `profilePicture`, `block`,
85
+ `unblock`, `modifyChat`, `labelChat`, `labelMessage`, `editLabel`, `deleteLabel`, `saveQuickReply`, `chats`, `contacts`,
86
+ `messages`, `search`, `botList`, `reachoutTimelock`, `newChatMessageCap`, `disappearingModes`, `stats`, `pendingSends`,
87
+ `ping`, `elapsed`, `requestPairCode` y `logout`. Para cualquier otro, `bot.call(método, parámetros)`.
88
+
89
+ `sendMedia` y compañía mandan los bytes por el WebSocket. `sendFile` y `downloadToFile` usan rutas **del servidor** y
90
+ son la opción para archivos grandes.
91
+
92
+ ## Aviso
93
+
94
+ SoblendWA es independiente y no está afiliado a WhatsApp ni a Meta. Automatizar una cuenta puede ir contra sus Términos
95
+ de Servicio y provocar un bloqueo. Lee
96
+ [qué está probado](https://github.com/Soblend-Development-Studio/SoblendWA#estado).
97
+
98
+ MIT © 2026 Soblend Development Studio
package/index.d.ts ADDED
@@ -0,0 +1,255 @@
1
+ export interface ConnectOptions {
2
+ session?: string
3
+ token?: string
4
+ reconnect?: boolean
5
+ timeout?: number
6
+ }
7
+
8
+ export interface MessageInfo {
9
+ id: string
10
+ chat: string
11
+ sender: string
12
+ senderAlt: string | null
13
+ senderPhone: string | null
14
+ senderLid: string | null
15
+ fromMe: boolean
16
+ isGroup: boolean
17
+ pushName: string | null
18
+ timestamp: number
19
+ category: string | null
20
+ offline: boolean
21
+ edit: string | null
22
+ serverId: string | null
23
+ elapsedMicros: number
24
+ }
25
+
26
+ export interface MediaInfo {
27
+ type: 'image' | 'video' | 'gif' | 'ptv' | 'audio' | 'ptt' | 'document' | 'sticker'
28
+ mimetype: string | null
29
+ size: number | null
30
+ caption?: string | null
31
+ seconds?: number | null
32
+ width?: number | null
33
+ height?: number | null
34
+ fileName?: string | null
35
+ title?: string | null
36
+ pages?: number | null
37
+ animated?: boolean
38
+ viewOnce: boolean
39
+ }
40
+
41
+ export interface QuotedInfo {
42
+ id: string
43
+ sender: string | null
44
+ chat: string | null
45
+ text: string | null
46
+ mediaType: string | null
47
+ media: MediaInfo | null
48
+ }
49
+
50
+ export interface SendTimings {
51
+ queuedMicros: number
52
+ uploadMicros: number | null
53
+ prepareMicros: number
54
+ encryptMicros: number
55
+ serverMicros: number | null
56
+ totalMicros: number
57
+ }
58
+
59
+ export interface Stats {
60
+ connected: boolean
61
+ uptimeSecs: number
62
+ received: number
63
+ sent: number
64
+ undecryptable: number
65
+ recovered: number
66
+ duplicates: number
67
+ awaitingRecovery: number
68
+ reconnects: number
69
+ rateLimited: number
70
+ mediaUploaded: number
71
+ mediaReused: number
72
+ outboxResent: number
73
+ panics: number
74
+ cachedDevices: number
75
+ cachedGroups: number
76
+ pendingQueries: number
77
+ openLanes: number
78
+ latencyMicros: number | null
79
+ }
80
+
81
+ export interface LinkPreviewOverride {
82
+ url?: string
83
+ title?: string
84
+ description?: string
85
+ thumbnail?: string
86
+ image?: string
87
+ width?: number
88
+ height?: number
89
+ video?: boolean
90
+ }
91
+
92
+ export interface PendingSend {
93
+ id: string
94
+ chat: string
95
+ queuedAt: number
96
+ attempts: number
97
+ }
98
+
99
+ export interface MessageEvent {
100
+ type: 'message'
101
+ info: MessageInfo
102
+ kind: string
103
+ mediaType: string | null
104
+ text: string | null
105
+ media: MediaInfo | null
106
+ quoted: QuotedInfo | null
107
+ viewOnce: boolean
108
+ ageSecs: number
109
+ linkPreview: { url: string | null; title: string | null; description: string | null; video: boolean; hasThumbnail: boolean; hasHighQualityThumbnail: boolean; thumbnailWidth: number | null; thumbnailHeight: number | null } | null
110
+ quotedId: string | null
111
+ mentions: string[]
112
+ reaction: { targetId: string | null; emoji: string | null } | null
113
+ reply: { id: string; text: string | null; source: 'buttons' | 'list' | 'template' | 'nativeFlow'; flow: string | null; params: unknown } | null
114
+ proto: string
115
+ }
116
+
117
+ export type SoblendEvent =
118
+ | MessageEvent
119
+ | { type: 'qr'; code: string }
120
+ | { type: 'pairSuccess'; id: string; lid: string | null; platform: string | null; businessName: string | null }
121
+ | { type: 'connected' }
122
+ | { type: 'disconnected'; reason: { type: string; detail?: string }; reconnecting: boolean }
123
+ | { type: 'loggedOut'; reason: string }
124
+ | { type: 'messageRecovered'; id: string; chat: string; sender: string }
125
+ | { type: 'accountBlocked'; code: number; reason: string; expires: number | null }
126
+ | { type: 'receipt'; chat: string; sender: string; ids: string[]; receipt: string; timestamp: number; fromMe: boolean }
127
+ | { type: 'serverAck'; id: string; chat: string; error: number | null; latencyMicros: number | null; timings: SendTimings | null }
128
+ | { type: 'pollVote'; chat: string; pollId: string; voter: string; selected: string[]; timestamp: number }
129
+ | { type: 'eventResponse'; chat: string; eventId: string; responder: string; response: string; extraGuests: number; timestamp: number }
130
+ | { type: 'chatUpdate'; update: { type: string; [field: string]: unknown }; timestamp: number; initialSync: boolean }
131
+ | { type: string; [field: string]: unknown }
132
+
133
+ export interface MediaOptions {
134
+ caption?: string
135
+ mimetype?: string
136
+ fileName?: string
137
+ voiceNote?: boolean
138
+ gif?: boolean
139
+ viewOnce?: boolean
140
+ seconds?: number
141
+ quoted?: MessageEvent | string
142
+ }
143
+
144
+ export interface ListSection {
145
+ title: string
146
+ rows: { id: string; title: string; description?: string }[]
147
+ }
148
+
149
+ export type InteractiveButton =
150
+ | { type: 'reply'; id: string; text: string }
151
+ | { type: 'url'; text: string; url: string }
152
+ | { type: 'copy'; text: string; code: string }
153
+ | { type: 'call'; text: string; phone: string }
154
+ | { type: 'select'; title: string; sections: ListSection[] }
155
+
156
+ export interface StoredMessage {
157
+ id: string
158
+ chat: string
159
+ sender: string | null
160
+ fromMe: boolean
161
+ timestamp: number
162
+ pushName: string | null
163
+ text: string | null
164
+ kind: string
165
+ status: 'error' | 'pending' | 'sent' | 'delivered' | 'read' | 'played'
166
+ starred: boolean
167
+ edited: boolean
168
+ revoked: boolean
169
+ reactions: Record<string, string>
170
+ pollVotes: Record<string, string[]>
171
+ }
172
+
173
+ type MessageRef = MessageEvent | string
174
+ type Bytes = Uint8Array | ArrayBuffer | string
175
+
176
+ export declare class SoblendClient {
177
+ static connect(url?: string, options?: ConnectOptions): Promise<SoblendClient>
178
+ constructor(url?: string, options?: ConnectOptions)
179
+ readonly me: string | null
180
+ readonly connected: boolean
181
+ readonly qr: string | null
182
+ open(): Promise<this>
183
+ close(): void
184
+ call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>
185
+ events(): AsyncGenerator<SoblendEvent>
186
+ on(event: 'message', listener: (event: MessageEvent) => unknown): this
187
+ on(event: 'open' | 'close' | 'lagged' | 'error', listener: (value: any) => unknown): this
188
+ on(event: string, listener: (event: SoblendEvent) => unknown): this
189
+ once(event: string, listener: (event: any) => unknown): this
190
+ off(event: string, listener: (event: any) => unknown): this
191
+
192
+ status(): Promise<{ me: string | null; connected: boolean }>
193
+ ping(): Promise<number>
194
+ stats(): Promise<Stats>
195
+ pendingSends(): Promise<PendingSend[]>
196
+ elapsed(message: MessageRef): Promise<number | null>
197
+ requestPairCode(phone: string | number): Promise<string>
198
+ logout(): Promise<void>
199
+
200
+ sendText(chat: string, text: string, options?: { quoted?: MessageRef; preview?: boolean | LinkPreviewOverride }): Promise<string>
201
+ reply(message: MessageRef, text: string): Promise<string>
202
+ react(message: MessageRef, emoji: string): Promise<string>
203
+ read(message: MessageRef): Promise<void>
204
+ markPlayed(message: MessageRef): Promise<void>
205
+ sendMedia(chat: string, kind: 'image' | 'video' | 'audio' | 'document' | 'sticker', data: Bytes, options?: MediaOptions): Promise<string>
206
+ sendImage(chat: string, data: Bytes, options?: MediaOptions): Promise<string>
207
+ sendVideo(chat: string, data: Bytes, options?: MediaOptions): Promise<string>
208
+ sendAudio(chat: string, data: Bytes, options?: MediaOptions): Promise<string>
209
+ sendDocument(chat: string, data: Bytes, options?: MediaOptions): Promise<string>
210
+ sendSticker(chat: string, data: Bytes, options?: MediaOptions): Promise<string>
211
+ sendFile(chat: string, kind: 'image' | 'video' | 'audio' | 'document' | 'sticker', path: string, options?: MediaOptions): Promise<string>
212
+ download(message: MessageRef): Promise<Uint8Array>
213
+ downloadToFile(message: MessageRef, path: string): Promise<number>
214
+ sendPoll(chat: string, name: string, options: string[], selectable?: number): Promise<string>
215
+ votePoll(chat: string, pollId: string, options: string[]): Promise<string>
216
+ edit(chat: string, messageId: string, text: string): Promise<string>
217
+ revoke(chat: string, messageId: string): Promise<string>
218
+ typing(chat: string, state?: 'composing' | 'recording' | 'paused'): Promise<void>
219
+ setOnline(available?: boolean): Promise<void>
220
+ sendInteractive(chat: string, spec: { body: string; buttons: InteractiveButton[]; title?: string; subtitle?: string; footer?: string; quoted?: MessageRef }): Promise<string>
221
+ sendButtons(chat: string, text: string, buttons: { id: string; text: string }[], footer?: string): Promise<string>
222
+ sendList(chat: string, list: { title: string; text: string; buttonText: string; sections: ListSection[]; footer?: string }): Promise<string>
223
+ sendContacts(chat: string, cards: { name: string; phone: string | number }[]): Promise<string>
224
+ sendMentionAll(chat: string, text: string): Promise<string>
225
+ sendGroupInvite(to: string, group: string, caption?: string): Promise<string>
226
+ setMemberLabel(group: string, label: string): Promise<string>
227
+ sendProto(chat: string, bytes: Bytes): Promise<string>
228
+
229
+ groupInfo(chat: string): Promise<Record<string, unknown>>
230
+ joinedGroups(): Promise<Record<string, unknown>[]>
231
+ onWhatsApp(...phones: (string | number | (string | number)[])[]): Promise<{ query: string; jid: string; exists: boolean; lid: string | null }[]>
232
+ profilePicture(jid: string, fullSize?: boolean): Promise<string | null>
233
+ block(jid: string): Promise<void>
234
+ unblock(jid: string): Promise<void>
235
+
236
+ modifyChat(chat: string, action: 'archive' | 'unarchive' | 'pin' | 'unpin' | 'mute' | 'unmute' | 'read' | 'unread' | 'delete' | 'clear', seconds?: number): Promise<void>
237
+ labelChat(chat: string, labelId: string | number, labeled?: boolean): Promise<void>
238
+ labelMessage(chat: string, message: MessageRef, labelId: string | number, labeled?: boolean): Promise<void>
239
+ editLabel(id: string | number, name: string, color?: number): Promise<void>
240
+ deleteLabel(id: string | number): Promise<void>
241
+ saveQuickReply(shortcut: string, text: string): Promise<string>
242
+
243
+ chats(): Promise<{ jid: string; name: string | null; unread: number; lastMessageAt: number; archived: boolean; pinned: boolean; mutedUntil: number | null; locked: boolean; labels: string[] }[]>
244
+ contacts(): Promise<{ jid: string; name: string | null; firstName: string | null; pushName: string | null; lid: string | null; phone: string | null }[]>
245
+ messages(chat: string, options?: { limit?: number; before?: number }): Promise<StoredMessage[]>
246
+ search(text: string, limit?: number): Promise<StoredMessage[]>
247
+
248
+ botList(): Promise<{ jid: string; personaId: string | null }[]>
249
+ reachoutTimelock(): Promise<{ active: boolean; endsAt: number | null; enforcement: string | null }>
250
+ newChatMessageCap(): Promise<unknown>
251
+ disappearingModes(...jids: (string | string[])[]): Promise<{ jid: string; seconds: number; setAt: number }[]>
252
+ }
253
+
254
+ export declare function connect(url?: string, options?: ConnectOptions): Promise<SoblendClient>
255
+ export default SoblendClient
package/index.js ADDED
@@ -0,0 +1,444 @@
1
+ const DEFAULT_URL = 'ws://127.0.0.1:8787'
2
+
3
+ class Emitter {
4
+ constructor() {
5
+ this.listeners = new Map()
6
+ }
7
+
8
+ on(type, listener) {
9
+ if (!this.listeners.has(type)) this.listeners.set(type, new Set())
10
+ this.listeners.get(type).add(listener)
11
+ return this
12
+ }
13
+
14
+ off(type, listener) {
15
+ this.listeners.get(type)?.delete(listener)
16
+ return this
17
+ }
18
+
19
+ once(type, listener) {
20
+ const wrapped = (value) => {
21
+ this.off(type, wrapped)
22
+ listener(value)
23
+ }
24
+ return this.on(type, wrapped)
25
+ }
26
+
27
+ emit(type, value) {
28
+ for (const listener of [...(this.listeners.get(type) ?? [])]) {
29
+ try {
30
+ const outcome = listener(value)
31
+ if (outcome && typeof outcome.catch === 'function') outcome.catch((error) => this.emit('error', error))
32
+ } catch (error) {
33
+ if (type !== 'error') this.emit('error', error)
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ async function socketClass() {
40
+ if (typeof globalThis.WebSocket === 'function') return globalThis.WebSocket
41
+ try {
42
+ const { WebSocket } = await import('ws')
43
+ return WebSocket
44
+ } catch {
45
+ throw new Error('This runtime has no WebSocket: use Node 22+, Bun, Deno or a browser, or install the "ws" package')
46
+ }
47
+ }
48
+
49
+ function toBase64(data) {
50
+ if (typeof data === 'string') return data
51
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
52
+ if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64')
53
+ let binary = ''
54
+ for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
55
+ return btoa(binary)
56
+ }
57
+
58
+ function fromBase64(text) {
59
+ if (typeof Buffer !== 'undefined') return Buffer.from(text, 'base64')
60
+ return Uint8Array.from(atob(text), (c) => c.charCodeAt(0))
61
+ }
62
+
63
+ function idOf(message) {
64
+ if (typeof message === 'string') return message
65
+ return message?.info?.id ?? message?.id
66
+ }
67
+
68
+ function media({ quoted, ...options } = {}) {
69
+ return { ...options, quotedId: quoted ? idOf(quoted) : undefined }
70
+ }
71
+
72
+ export class SoblendClient extends Emitter {
73
+ static async connect(url = DEFAULT_URL, options = {}) {
74
+ const client = new SoblendClient(url, options)
75
+ await client.open()
76
+ return client
77
+ }
78
+
79
+ constructor(url = DEFAULT_URL, { session = 'default', token, reconnect = true, timeout = 120000 } = {}) {
80
+ super()
81
+ this.base = String(url).replace(/^http/, 'ws').replace(/\/+$/, '')
82
+ this.session = session
83
+ this.token = token
84
+ this.reconnect = reconnect
85
+ this.timeout = timeout
86
+ this.pending = new Map()
87
+ this.counter = 0
88
+ this.state = { me: null, connected: false, qr: null, methods: [] }
89
+ this.closing = false
90
+ this.attempt = 0
91
+ }
92
+
93
+ get me() {
94
+ return this.state.me
95
+ }
96
+
97
+ get connected() {
98
+ return this.state.connected
99
+ }
100
+
101
+ get qr() {
102
+ return this.state.qr
103
+ }
104
+
105
+ async open() {
106
+ const Socket = await socketClass()
107
+ const query = this.token ? `?token=${encodeURIComponent(this.token)}` : ''
108
+ const socket = new Socket(`${this.base}/ws/${encodeURIComponent(this.session)}${query}`)
109
+ this.socket = socket
110
+ return new Promise((resolve, reject) => {
111
+ let greeted = false
112
+ socket.onmessage = (message) => {
113
+ const frame = JSON.parse(typeof message.data === 'string' ? message.data : String(message.data))
114
+ if (frame.type === 'hello') {
115
+ greeted = true
116
+ this.attempt = 0
117
+ Object.assign(this.state, { me: frame.me, connected: frame.connected, qr: frame.qr, methods: frame.methods })
118
+ this.emit('open', frame)
119
+ resolve(this)
120
+ return
121
+ }
122
+ this.handle(frame)
123
+ }
124
+ socket.onerror = (error) => {
125
+ if (!greeted) reject(new Error(`could not reach SoblendWA at ${this.base}: ${error?.message ?? 'connection failed'}`))
126
+ }
127
+ socket.onclose = (event) => {
128
+ this.failPending(new Error('the connection to the SoblendWA server closed'))
129
+ this.emit('close', { code: event?.code, reason: event?.reason })
130
+ if (!greeted) reject(new Error(`SoblendWA refused the connection (${event?.code ?? 'closed'}): check the url and token`))
131
+ if (greeted && this.reconnect && !this.closing) this.retry()
132
+ }
133
+ })
134
+ }
135
+
136
+ retry() {
137
+ const delay = Math.min(30000, 500 * 2 ** this.attempt++)
138
+ setTimeout(() => {
139
+ this.open().catch((error) => {
140
+ this.emit('error', error)
141
+ if (this.reconnect && !this.closing) this.retry()
142
+ })
143
+ }, delay)
144
+ }
145
+
146
+ handle(frame) {
147
+ if (frame.id !== undefined && frame.id !== null && this.pending.has(frame.id)) {
148
+ const { resolve, reject, timer } = this.pending.get(frame.id)
149
+ this.pending.delete(frame.id)
150
+ clearTimeout(timer)
151
+ if (frame.error) reject(new Error(frame.error.message))
152
+ else resolve(frame.result)
153
+ return
154
+ }
155
+ if (frame.type === 'lagged') {
156
+ this.emit('lagged', frame.missed)
157
+ return
158
+ }
159
+ const event = frame.event
160
+ if (!event) return
161
+ if (event.type === 'qr') this.state.qr = event.code
162
+ if (event.type === 'connected') Object.assign(this.state, { connected: true, qr: null })
163
+ if (event.type === 'disconnected') this.state.connected = false
164
+ if (event.type === 'pairSuccess') this.state.me = event.id
165
+ if (event.type === 'loggedOut') Object.assign(this.state, { connected: false, me: null })
166
+ this.emit(event.type, event)
167
+ this.emit('event', event)
168
+ }
169
+
170
+ failPending(error) {
171
+ for (const { reject, timer } of this.pending.values()) {
172
+ clearTimeout(timer)
173
+ reject(error)
174
+ }
175
+ this.pending.clear()
176
+ }
177
+
178
+ call(method, params = {}) {
179
+ if (!this.socket || this.socket.readyState !== 1) {
180
+ return Promise.reject(new Error('not connected to the SoblendWA server'))
181
+ }
182
+ const id = ++this.counter
183
+ return new Promise((resolve, reject) => {
184
+ const timer = setTimeout(() => {
185
+ this.pending.delete(id)
186
+ reject(new Error(`${method} timed out`))
187
+ }, this.timeout)
188
+ this.pending.set(id, { resolve, reject, timer })
189
+ this.socket.send(JSON.stringify({ id, method, params }))
190
+ })
191
+ }
192
+
193
+ async *events() {
194
+ const queue = []
195
+ let wake = null
196
+ const push = (event) => {
197
+ queue.push(event)
198
+ wake?.()
199
+ }
200
+ this.on('event', push)
201
+ try {
202
+ for (;;) {
203
+ while (queue.length) yield queue.shift()
204
+ await new Promise((resolve) => (wake = resolve))
205
+ wake = null
206
+ }
207
+ } finally {
208
+ this.off('event', push)
209
+ }
210
+ }
211
+
212
+ close() {
213
+ this.closing = true
214
+ this.failPending(new Error('client closed'))
215
+ this.socket?.close()
216
+ }
217
+
218
+ status() {
219
+ return this.call('status')
220
+ }
221
+
222
+ ping() {
223
+ return this.call('ping')
224
+ }
225
+
226
+ stats() {
227
+ return this.call('stats')
228
+ }
229
+
230
+ pendingSends() {
231
+ return this.call('pendingSends')
232
+ }
233
+
234
+ elapsed(message) {
235
+ return this.call('elapsed', { messageId: idOf(message) })
236
+ }
237
+
238
+ requestPairCode(phone) {
239
+ return this.call('requestPairCode', { phone: String(phone) })
240
+ }
241
+
242
+ logout() {
243
+ return this.call('logout')
244
+ }
245
+
246
+ sendText(chat, text, { quoted, preview } = {}) {
247
+ return this.call('sendText', { chat, text, quotedId: quoted ? idOf(quoted) : undefined, preview })
248
+ }
249
+
250
+ reply(message, text) {
251
+ return this.call('reply', { messageId: idOf(message), text })
252
+ }
253
+
254
+ react(message, emoji) {
255
+ return this.call('react', { messageId: idOf(message), emoji })
256
+ }
257
+
258
+ read(message) {
259
+ return this.call('read', { messageId: idOf(message) })
260
+ }
261
+
262
+ markPlayed(message) {
263
+ return this.call('markPlayed', { messageId: idOf(message) })
264
+ }
265
+
266
+ sendMedia(chat, kind, data, options = {}) {
267
+ return this.call('sendMedia', { chat, kind, data: toBase64(data), ...media(options) })
268
+ }
269
+
270
+ sendImage(chat, data, options) {
271
+ return this.sendMedia(chat, 'image', data, options)
272
+ }
273
+
274
+ sendVideo(chat, data, options) {
275
+ return this.sendMedia(chat, 'video', data, options)
276
+ }
277
+
278
+ sendAudio(chat, data, options) {
279
+ return this.sendMedia(chat, 'audio', data, options)
280
+ }
281
+
282
+ sendDocument(chat, data, options) {
283
+ return this.sendMedia(chat, 'document', data, options)
284
+ }
285
+
286
+ sendSticker(chat, data, options) {
287
+ return this.sendMedia(chat, 'sticker', data, options)
288
+ }
289
+
290
+ sendFile(chat, kind, path, options = {}) {
291
+ return this.call('sendFile', { chat, kind, path: String(path), ...media(options) })
292
+ }
293
+
294
+ async download(message) {
295
+ const { base64 } = await this.call('download', { messageId: idOf(message) })
296
+ return fromBase64(base64)
297
+ }
298
+
299
+ downloadToFile(message, path) {
300
+ return this.call('downloadToFile', { messageId: idOf(message), path: String(path) })
301
+ }
302
+
303
+ sendPoll(chat, name, options, selectable = 1) {
304
+ return this.call('sendPoll', { chat, name, options, selectable })
305
+ }
306
+
307
+ votePoll(chat, pollId, options) {
308
+ return this.call('votePoll', { chat, pollId, options })
309
+ }
310
+
311
+ edit(chat, messageId, text) {
312
+ return this.call('edit', { chat, messageId, text })
313
+ }
314
+
315
+ revoke(chat, messageId) {
316
+ return this.call('revoke', { chat, messageId })
317
+ }
318
+
319
+ typing(chat, state = 'composing') {
320
+ return this.call('typing', { chat, state })
321
+ }
322
+
323
+ setOnline(available = true) {
324
+ return this.call('setOnline', { available })
325
+ }
326
+
327
+ sendInteractive(chat, { quoted, ...spec }) {
328
+ return this.call('sendInteractive', { chat, ...spec, quotedId: quoted ? idOf(quoted) : undefined })
329
+ }
330
+
331
+ sendButtons(chat, text, buttons, footer) {
332
+ return this.call('sendButtons', { chat, text, buttons, footer })
333
+ }
334
+
335
+ sendList(chat, { title, text, buttonText, sections, footer }) {
336
+ return this.call('sendList', { chat, title, text, buttonText, sections, footer })
337
+ }
338
+
339
+ sendContacts(chat, cards) {
340
+ return this.call('sendContacts', { chat, cards })
341
+ }
342
+
343
+ sendMentionAll(chat, text) {
344
+ return this.call('sendMentionAll', { chat, text })
345
+ }
346
+
347
+ sendGroupInvite(to, group, caption) {
348
+ return this.call('sendGroupInvite', { to, group, caption })
349
+ }
350
+
351
+ setMemberLabel(group, label) {
352
+ return this.call('setMemberLabel', { group, label })
353
+ }
354
+
355
+ sendProto(chat, bytes) {
356
+ return this.call('sendProto', { chat, proto: toBase64(bytes) })
357
+ }
358
+
359
+ groupInfo(chat) {
360
+ return this.call('groupInfo', { chat })
361
+ }
362
+
363
+ joinedGroups() {
364
+ return this.call('joinedGroups')
365
+ }
366
+
367
+ onWhatsApp(...phones) {
368
+ return this.call('onWhatsApp', { phones: phones.flat().map(String) })
369
+ }
370
+
371
+ profilePicture(jid, fullSize = false) {
372
+ return this.call('profilePicture', { jid, fullSize })
373
+ }
374
+
375
+ block(jid) {
376
+ return this.call('block', { jid })
377
+ }
378
+
379
+ unblock(jid) {
380
+ return this.call('unblock', { jid })
381
+ }
382
+
383
+ modifyChat(chat, action, seconds) {
384
+ return this.call('modifyChat', { chat, action, seconds })
385
+ }
386
+
387
+ labelChat(chat, labelId, labeled = true) {
388
+ return this.call('labelChat', { chat, labelId: String(labelId), labeled })
389
+ }
390
+
391
+ labelMessage(chat, message, labelId, labeled = true) {
392
+ return this.call('labelMessage', { chat, messageId: idOf(message), labelId: String(labelId), labeled })
393
+ }
394
+
395
+ editLabel(id, name, color = 0) {
396
+ return this.call('editLabel', { id: String(id), name, color })
397
+ }
398
+
399
+ deleteLabel(id) {
400
+ return this.call('deleteLabel', { id: String(id) })
401
+ }
402
+
403
+ saveQuickReply(shortcut, text) {
404
+ return this.call('saveQuickReply', { shortcut, text })
405
+ }
406
+
407
+ chats() {
408
+ return this.call('chats')
409
+ }
410
+
411
+ contacts() {
412
+ return this.call('contacts')
413
+ }
414
+
415
+ messages(chat, { limit = 50, before } = {}) {
416
+ return this.call('messages', { chat, limit, before })
417
+ }
418
+
419
+ search(text, limit = 50) {
420
+ return this.call('search', { text, limit })
421
+ }
422
+
423
+ botList() {
424
+ return this.call('botList')
425
+ }
426
+
427
+ reachoutTimelock() {
428
+ return this.call('reachoutTimelock')
429
+ }
430
+
431
+ newChatMessageCap() {
432
+ return this.call('newChatMessageCap')
433
+ }
434
+
435
+ disappearingModes(...jids) {
436
+ return this.call('disappearingModes', { jids: jids.flat().map(String) })
437
+ }
438
+ }
439
+
440
+ export function connect(url, options) {
441
+ return SoblendClient.connect(url, options)
442
+ }
443
+
444
+ export default SoblendClient
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "soblendwa-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "JavaScript and TypeScript client for the SoblendWA server: WhatsApp bots in any runtime, powered by Rust. By Soblend Development Studio.",
5
+ "author": "Soblend Development Studio",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Soblend-Development-Studio/SoblendWA/tree/main/clients/js#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Soblend-Development-Studio/SoblendWA.git",
11
+ "directory": "clients/js"
12
+ },
13
+ "bugs": { "url": "https://github.com/Soblend-Development-Studio/SoblendWA/issues" },
14
+ "type": "module",
15
+ "main": "index.js",
16
+ "types": "index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./index.d.ts",
20
+ "default": "./index.js"
21
+ }
22
+ },
23
+ "files": ["index.js", "index.d.ts", "README.md"],
24
+ "engines": { "node": ">=18" },
25
+ "keywords": ["whatsapp", "baileys", "bot", "websocket", "typescript", "rust"],
26
+ "peerDependencies": {
27
+ "ws": ">=8"
28
+ },
29
+ "peerDependenciesMeta": {
30
+ "ws": { "optional": true }
31
+ },
32
+ "scripts": {
33
+ "test": "node test.mjs"
34
+ }
35
+ }