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.
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/index.d.ts +255 -0
- package/index.js +444 -0
- 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
|
-
#
|
|
1
|
+
# soblendwa-client
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|