@zervonex/baileys 3.0.6 → 3.0.7
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/README.md +31 -25
- package/lib/Utils/use-multi-file-auth-state.js +41 -43
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -350,15 +350,14 @@ Two adapters are available. They do **not** share an identical return shape —
|
|
|
350
350
|
|
|
351
351
|
### File-based (default)
|
|
352
352
|
|
|
353
|
-
Atomic writes
|
|
353
|
+
Atomic writes (tmp → rename with fallback), corrupt file self-healing, and automatic old pre-key cleanup. Best for single-instance bots or local development.
|
|
354
354
|
|
|
355
355
|
```javascript
|
|
356
356
|
import { useMultiFileAuthState } from '@zervonex/baileys'
|
|
357
357
|
|
|
358
358
|
const { state, saveCreds, getStats } = await useMultiFileAuthState('auth_session', {
|
|
359
|
-
preKeyRetention:
|
|
360
|
-
cleanupThreshold:
|
|
361
|
-
logger, // optional — logs checksum mismatches and cleanup events
|
|
359
|
+
preKeyRetention: 150, // how many recent prekeys to keep (default: 150)
|
|
360
|
+
cleanupThreshold: 50, // new prekeys before a cleanup runs (default: 50)
|
|
362
361
|
})
|
|
363
362
|
|
|
364
363
|
const sock = makeWASocket({ auth: state })
|
|
@@ -369,15 +368,20 @@ const stats = await getStats()
|
|
|
369
368
|
// { totalFiles: 42, preKeyCount: 150, nextPreKeyId: 3200, lastCleanupAt: '2026-07-05T...' }
|
|
370
369
|
```
|
|
371
370
|
|
|
372
|
-
To fully log out and delete a session
|
|
371
|
+
To fully log out and delete a session, remove the session folder directly — there is no `clearSession` method on this adapter:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
import { rm } from 'fs/promises'
|
|
375
|
+
await rm('auth_session', { recursive: true, force: true })
|
|
376
|
+
```
|
|
373
377
|
|
|
374
378
|
### Keyv-based (production)
|
|
375
379
|
|
|
376
|
-
Universal adapter for any database backend via [Keyv](https://
|
|
380
|
+
Universal adapter for any database backend via [Keyv](https://www.npmjs.com/package/keyv). Pass a connection string — the matching backend driver is resolved and cached automatically, no manual adapter imports needed.
|
|
377
381
|
|
|
378
|
-
|
|
382
|
+
Both adapters use the same `BufferJSON` serialization format and auto-detect legacy `{ value: { value: ... } }` double-wrapping from older versions — no manual data migration needed when upgrading.
|
|
379
383
|
|
|
380
|
-
The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It
|
|
384
|
+
The first argument is the **session ID** — equivalent to the folder name in `useMultiFileAuthState`. It is used as a Keyv namespace so multiple bots can safely share one backend without key collisions. The second argument is a connection string, a pre-built `Keyv` instance, or a raw `KeyvStoreAdapter`:
|
|
381
385
|
|
|
382
386
|
```javascript
|
|
383
387
|
import { useKeyvAuthState } from '@zervonex/baileys'
|
|
@@ -407,52 +411,56 @@ const { state, saveCreds } = await useKeyvAuthState('bot1', 'memcache://localhos
|
|
|
407
411
|
const { state, saveCreds } = await useKeyvAuthState('test')
|
|
408
412
|
```
|
|
409
413
|
|
|
410
|
-
Supported
|
|
414
|
+
Supported protocols: `redis:`/`rediss:`, `mongodb:`/`mongodb+srv:`, `postgres:`/`postgresql:`, `mysql:`, `sqlite:`, `etcd:`, `memcache:`/`memcached:`. All matching driver packages ship as dependencies — nothing extra to install.
|
|
411
415
|
|
|
412
|
-
For backends without a connection string (e.g. DynamoDB), or to
|
|
416
|
+
For backends without a connection string (e.g. DynamoDB), or to share one connection across multiple sessions, pass a pre-built adapter or `Keyv` instance:
|
|
413
417
|
|
|
414
418
|
```javascript
|
|
415
|
-
import { useKeyvAuthState } from '@zervonex/baileys'
|
|
416
419
|
import KeyvMongo from '@keyv/mongo'
|
|
420
|
+
import KeyvDynamo from '@keyv/dynamo'
|
|
421
|
+
|
|
422
|
+
// share one connection across sessions
|
|
423
|
+
const mongo = new KeyvMongo('mongodb://localhost/baileys')
|
|
424
|
+
const { state: s1 } = await useKeyvAuthState('bot1', mongo)
|
|
425
|
+
const { state: s2 } = await useKeyvAuthState('bot2', mongo)
|
|
417
426
|
|
|
418
|
-
|
|
419
|
-
const
|
|
427
|
+
// DynamoDB (no connection string protocol)
|
|
428
|
+
const dynamo = new KeyvDynamo({ region: 'us-east-1', table: 'baileys_auth' })
|
|
429
|
+
const { state, saveCreds } = await useKeyvAuthState('bot1', dynamo)
|
|
420
430
|
```
|
|
421
431
|
|
|
422
|
-
|
|
432
|
+
Connection strings passed as plain strings are automatically cached and reused across every `useKeyvAuthState` call in the process — no need to build the instance yourself just to avoid duplicate connections.
|
|
423
433
|
|
|
424
|
-
By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv
|
|
434
|
+
By default, Mongo/Postgres/MySQL/SQLite store everything in a collection/table named `keyv`. Pass `collectionName` to use your own:
|
|
425
435
|
|
|
426
436
|
```javascript
|
|
427
437
|
const { state, saveCreds } = await useKeyvAuthState('bot1', 'mongodb://localhost/baileys', {
|
|
428
|
-
collectionName: 'baileys_auth',
|
|
438
|
+
collectionName: 'baileys_auth',
|
|
429
439
|
})
|
|
430
440
|
```
|
|
431
441
|
|
|
432
|
-
> Per-type backend routing (e.g. prekeys on SQLite, sessions on Postgres) is not currently supported — one `useKeyvAuthState` call uses one backend for
|
|
442
|
+
> Per-type backend routing (e.g. prekeys on SQLite, sessions on Postgres) is not currently supported — one `useKeyvAuthState` call uses one backend for all key types.
|
|
433
443
|
|
|
434
444
|
#### Clearing a session (logout)
|
|
435
445
|
|
|
436
446
|
```javascript
|
|
437
447
|
const { state, saveCreds, clearSession } = await useKeyvAuthState('bot1', 'redis://localhost:6379')
|
|
438
448
|
|
|
439
|
-
|
|
440
|
-
await clearSession()
|
|
449
|
+
await clearSession() // wipes all keys for this session, disconnects the store
|
|
441
450
|
```
|
|
442
451
|
|
|
443
|
-
`clearSession()`
|
|
452
|
+
`clearSession()` removes every key scoped to that session's namespace — creds, prekeys, sessions, sender keys, everything. Other sessions sharing the same backend are untouched.
|
|
444
453
|
|
|
445
454
|
#### Checking auth without loading full state
|
|
446
455
|
|
|
447
456
|
```javascript
|
|
448
457
|
import { hasKeyvAuth } from '@zervonex/baileys'
|
|
449
458
|
|
|
450
|
-
// Check if a session has valid auth (lightweight — no full state load)
|
|
451
459
|
const valid = await hasKeyvAuth('bot1', backend)
|
|
452
460
|
// true if creds exist and contain noiseKey + signedIdentityKey
|
|
453
461
|
```
|
|
454
462
|
|
|
455
|
-
|
|
463
|
+
Lightweight — opens no full auth state, just checks for valid creds under the session's namespace.
|
|
456
464
|
|
|
457
465
|
#### Options
|
|
458
466
|
|
|
@@ -460,8 +468,7 @@ const valid = await hasKeyvAuth('bot1', backend)
|
|
|
460
468
|
|--------|------|---------|-------------|
|
|
461
469
|
| `preKeyRetention` | `number` | `150` | Number of recent prekeys to keep |
|
|
462
470
|
| `cleanupThreshold` | `number` | `50` | New prekeys generated before cleanup runs |
|
|
463
|
-
| `
|
|
464
|
-
| `collectionName` | `string` | adapter's own default (`"keyv"`) | Custom collection/table name — Keyv-based adapter only, ignored for connection-string-less (in-memory) and pre-built adapter/`Keyv` instances |
|
|
471
|
+
| `collectionName` | `string` | `"keyv"` | Custom collection/table name (Keyv adapter only, ignored for in-memory and pre-built instances) |
|
|
465
472
|
|
|
466
473
|
#### Return values
|
|
467
474
|
|
|
@@ -471,7 +478,6 @@ const valid = await hasKeyvAuth('bot1', backend)
|
|
|
471
478
|
| `saveCreds` | ✅ | ✅ |
|
|
472
479
|
| `getStats` | ✅ | ❌ |
|
|
473
480
|
| `clearSession` | ❌ (delete the folder instead) | ✅ |
|
|
474
|
-
| `close` | ❌ | ✅ (drops local listeners; safe to call, does not disconnect a shared backend) |
|
|
475
481
|
|
|
476
482
|
---
|
|
477
483
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, stat, unlink, writeFile, readdir } from 'fs/promises'
|
|
2
2
|
import { join } from 'path'
|
|
3
|
-
import { Keyv } from 'keyv'
|
|
3
|
+
import { Keyv, detectKeyvStorage } from 'keyv'
|
|
4
4
|
import { proto } from '../../WAProto/index.js'
|
|
5
5
|
import { initAuthCreds } from './auth-utils.js'
|
|
6
6
|
import { BufferJSON } from './generics.js'
|
|
@@ -11,21 +11,16 @@ const DEFAULT_PREKEY_RETENTION = 150
|
|
|
11
11
|
const DEFAULT_CLEANUP_THRESHOLD = 50
|
|
12
12
|
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000
|
|
13
13
|
|
|
14
|
-
// ──
|
|
14
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
16
|
const bootstrapCreds = raw => raw ?? initAuthCreds()
|
|
17
|
-
|
|
18
|
-
const patchAppStateKey = (type, v) =>
|
|
19
|
-
type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
20
|
-
|
|
17
|
+
const patchAppStateKey = (type, v) => type === 'app-state-sync-key' && v ? proto.Message.AppStateSyncKeyData.fromObject(v) : v
|
|
21
18
|
const ser = v => v == null ? null : JSON.parse(JSON.stringify(v, BufferJSON.replacer))
|
|
22
|
-
|
|
23
19
|
const deser = raw => {
|
|
24
20
|
if (raw == null) return null
|
|
25
21
|
let p = typeof raw === 'string'
|
|
26
|
-
? (() => { try { return JSON.parse(raw) } catch (e) { logger.warn({ err: e.message }, '[Auth] failed to parse stored value
|
|
22
|
+
? (() => { try { return JSON.parse(raw) } catch (e) { logger.warn({ err: e.message }, '[Auth] failed to parse stored value'); return null } })()
|
|
27
23
|
: raw
|
|
28
|
-
// unwrap adapter double-wrapping e.g. { value: { value: actual } }
|
|
29
24
|
while (p && typeof p === 'object' && !Array.isArray(p) && Object.keys(p).length === 1 && 'value' in p) p = p.value
|
|
30
25
|
if (p == null) return null
|
|
31
26
|
if (typeof p === 'object' && '__checksum' in p && 'data' in p) p = p.data
|
|
@@ -34,7 +29,9 @@ const deser = raw => {
|
|
|
34
29
|
|
|
35
30
|
// ── Adapter resolution ────────────────────────────────────────────────────────
|
|
36
31
|
|
|
32
|
+
// Only adapters that accept a named collection/table — redis, valkey, etcd, memcache, dynamo do not
|
|
37
33
|
const COLLECTION_KEY = { '@keyv/mongo': 'collection', '@keyv/postgres': 'table', '@keyv/mysql': 'table', '@keyv/sqlite': 'table' }
|
|
34
|
+
|
|
38
35
|
const _adapterCache = new Map()
|
|
39
36
|
export const clearAdapterCache = () => _adapterCache.clear()
|
|
40
37
|
|
|
@@ -44,7 +41,7 @@ const resolveAdapter = async (conn, col) => {
|
|
|
44
41
|
let protocol
|
|
45
42
|
try { protocol = new URL(conn).protocol } catch { throw new Error(`[Auth] Invalid connection string: "${conn}"`) }
|
|
46
43
|
const pkg = PROTOCOL_ADAPTERS[protocol]
|
|
47
|
-
if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported:
|
|
44
|
+
if (!pkg) throw new Error(`[Auth] Unsupported protocol "${protocol}" — supported: ${Object.keys(PROTOCOL_ADAPTERS).map(p => p.replace(':', '')).join(', ')}`)
|
|
48
45
|
let mod
|
|
49
46
|
try { mod = await import(pkg) } catch (e) {
|
|
50
47
|
if (e.code === 'ERR_MODULE_NOT_FOUND') throw new Error(`[Auth] "${pkg}" required — npm i ${pkg}`)
|
|
@@ -58,12 +55,25 @@ const resolveAdapter = async (conn, col) => {
|
|
|
58
55
|
return adapter
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
// Object.create scoping only needed for mapLike stores — keyvStorage and asyncMap have namespace managed by Keyv/bridge
|
|
59
|
+
const resolveStore = async (backend, col, namespace) => {
|
|
62
60
|
if (backend == null) return null
|
|
63
61
|
if (typeof backend === 'string') return resolveAdapter(backend, col)
|
|
64
62
|
if (backend instanceof Keyv) return backend.opts?.store ?? backend.store ?? null
|
|
65
|
-
|
|
66
|
-
throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
|
|
63
|
+
const { compatible, store } = detectKeyvStorage(backend)
|
|
64
|
+
if (!compatible) throw new Error('[Auth] backend must be a connection string, Keyv instance, or KeyvStoreAdapter')
|
|
65
|
+
if (store === 'mapLike') {
|
|
66
|
+
const scoped = Object.create(backend)
|
|
67
|
+
if (namespace) scoped.namespace = namespace
|
|
68
|
+
return scoped
|
|
69
|
+
}
|
|
70
|
+
return backend
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const makeKeyv = (store, namespace) => {
|
|
74
|
+
const keyv = store ? new Keyv({ store, namespace }) : new Keyv({ namespace })
|
|
75
|
+
keyv.setMaxListeners?.(0)
|
|
76
|
+
return keyv
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
// ── File-based auth state ─────────────────────────────────────────────────────
|
|
@@ -77,13 +87,12 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
|
77
87
|
if (info) { if (!info.isDirectory()) throw new Error(`Not a directory: ${folder}`) }
|
|
78
88
|
else await mkdir(folder, { recursive: true })
|
|
79
89
|
|
|
80
|
-
//
|
|
90
|
+
// Atomic write: tmp → rename, fallback to direct write
|
|
81
91
|
const write = async (data, file) => {
|
|
82
92
|
const [p, t, payload] = [fp(file), fp(file) + '.tmp', JSON.stringify(data, BufferJSON.replacer)]
|
|
83
93
|
await writeFile(t, payload)
|
|
84
94
|
try { await rename(t, p) } catch { await writeFile(p, payload); await unlink(t).catch(() => { }) }
|
|
85
95
|
}
|
|
86
|
-
|
|
87
96
|
const read = async file => {
|
|
88
97
|
const raw = await readFile(fp(file), 'utf-8').catch(() => null)
|
|
89
98
|
if (!raw) return null
|
|
@@ -96,7 +105,6 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
|
96
105
|
return null
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
|
-
|
|
100
108
|
const remove = f => unlink(fp(f)).catch(() => { })
|
|
101
109
|
|
|
102
110
|
let creds = bootstrapCreds(await read('creds.json'))
|
|
@@ -114,11 +122,9 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
|
114
122
|
.map(m => join(folder, m[0]))
|
|
115
123
|
if (targets.length) await Promise.all(targets.map(f => unlink(f).catch(() => { })))
|
|
116
124
|
lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
|
|
117
|
-
} catch (e) {
|
|
118
|
-
|
|
119
|
-
} finally { cleanupRunning = false }
|
|
125
|
+
} catch (e) { logger.warn({ err: e.message }, 'prekey cleanup failed') }
|
|
126
|
+
finally { cleanupRunning = false }
|
|
120
127
|
}
|
|
121
|
-
|
|
122
128
|
cleanOldPreKeys().catch(() => { })
|
|
123
129
|
|
|
124
130
|
return {
|
|
@@ -153,19 +159,14 @@ export const useMultiFileAuthState = async (folder, opts = {}) => {
|
|
|
153
159
|
|
|
154
160
|
export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
155
161
|
const { preKeyRetention = DEFAULT_PREKEY_RETENTION, cleanupThreshold = DEFAULT_CLEANUP_THRESHOLD, collectionName } = opts
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
|
|
159
|
-
keyv.setMaxListeners?.(0)
|
|
162
|
+
const store = await resolveStore(backend, collectionName, sessionId)
|
|
163
|
+
const keyv = makeKeyv(store, sessionId)
|
|
160
164
|
keyv.on('error', err => logger.warn({ err: err?.message, sessionId }, '[Auth] store error'))
|
|
161
165
|
|
|
162
166
|
const kn = (type, id) => `${type}-${id}`
|
|
163
167
|
const credsKey = 'creds'
|
|
164
|
-
|
|
165
168
|
let creds = bootstrapCreds(deser(await keyv.get(credsKey)))
|
|
166
169
|
let cleanupRunning = false, lastCleanupAt = 0, lastCleanedId = creds.nextPreKeyId
|
|
167
|
-
// tracks every key ever written — fallback clearSession uses this to wipe all types including JID-based IDs
|
|
168
|
-
const _writtenKeys = new Set([credsKey])
|
|
169
170
|
|
|
170
171
|
const cleanOldPreKeys = async () => {
|
|
171
172
|
if (cleanupRunning || Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return
|
|
@@ -175,13 +176,11 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
|
175
176
|
if (minId <= 0) return
|
|
176
177
|
const ids = []
|
|
177
178
|
for (let id = Math.max(0, lastCleanedId - preKeyRetention); id < minId; id++) ids.push(kn('pre-key', id))
|
|
178
|
-
if (ids.length) await
|
|
179
|
+
if (ids.length) await keyv.deleteMany(ids)
|
|
179
180
|
lastCleanupAt = Date.now(); lastCleanedId = creds.nextPreKeyId
|
|
180
|
-
} catch (e) {
|
|
181
|
-
|
|
182
|
-
} finally { cleanupRunning = false }
|
|
181
|
+
} catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] prekey cleanup failed') }
|
|
182
|
+
finally { cleanupRunning = false }
|
|
183
183
|
}
|
|
184
|
-
|
|
185
184
|
cleanOldPreKeys().catch(() => { })
|
|
186
185
|
|
|
187
186
|
const keys = {
|
|
@@ -195,35 +194,34 @@ export const useKeyvAuthState = async (sessionId, backend, opts = {}) => {
|
|
|
195
194
|
for (const type in data)
|
|
196
195
|
for (const id in data[type]) {
|
|
197
196
|
const v = data[type][id], k = kn(type, id)
|
|
198
|
-
if (v != null)
|
|
199
|
-
else
|
|
197
|
+
if (v != null) sets.push({ key: k, value: ser(v) })
|
|
198
|
+
else dels.push(k)
|
|
200
199
|
}
|
|
201
|
-
await Promise.all([
|
|
202
|
-
sets.length ? keyv.setMany(sets) : null,
|
|
203
|
-
dels.length ? keyv.deleteMany(dels) : null
|
|
204
|
-
])
|
|
200
|
+
await Promise.all([sets.length ? keyv.setMany(sets) : null, dels.length ? keyv.deleteMany(dels) : null])
|
|
205
201
|
}
|
|
206
202
|
}
|
|
207
203
|
|
|
208
204
|
const saveCreds = async () => {
|
|
209
205
|
if (creds.nextPreKeyId - lastCleanedId >= cleanupThreshold) cleanOldPreKeys().catch(() => { })
|
|
210
|
-
_writtenKeys.add(credsKey)
|
|
211
206
|
await keyv.set(credsKey, ser(creds))
|
|
212
207
|
}
|
|
213
208
|
|
|
214
209
|
const clearSession = async () => {
|
|
215
|
-
|
|
210
|
+
try {
|
|
211
|
+
const isolated = makeKeyv(store, sessionId)
|
|
212
|
+
await isolated.clear()
|
|
213
|
+
isolated.removeAllListeners?.()
|
|
214
|
+
} catch (e) { logger.warn({ err: e.message, sessionId }, '[Auth] clearSession failed') }
|
|
216
215
|
keyv.removeAllListeners?.()
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
return { state: { creds, keys }, saveCreds, clearSession }
|
|
220
219
|
}
|
|
221
220
|
|
|
222
|
-
//
|
|
221
|
+
// Checks if valid creds exist for a session without fully loading it
|
|
223
222
|
export const hasKeyvAuth = async (sessionId, backend, opts = {}) => {
|
|
224
223
|
try {
|
|
225
|
-
const
|
|
226
|
-
const keyv = store ? new Keyv({ store, namespace: sessionId }) : new Keyv({ namespace: sessionId })
|
|
224
|
+
const keyv = makeKeyv(await resolveStore(backend, opts.collectionName, sessionId), sessionId)
|
|
227
225
|
const parsed = deser(await keyv.get('creds'))
|
|
228
226
|
return !!(parsed?.noiseKey && parsed?.signedIdentityKey)
|
|
229
227
|
} catch { return false }
|
package/lib/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const info = [
|
|
|
29
29
|
'╭──────────────────────────────────────────────────────────────╮',
|
|
30
30
|
'│ │',
|
|
31
31
|
'│ ◆ Package › @zervonex/baileys │',
|
|
32
|
-
'│ ◆ Version › 3.0.
|
|
32
|
+
'│ ◆ Version › 3.0.7 │',
|
|
33
33
|
'│ ◆ Status › Production Ready │',
|
|
34
34
|
'│ │',
|
|
35
35
|
'├──────────────────────────── ◇◆◇ ─────────────────────────────┤',
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@zervonex/baileys",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.0.
|
|
5
|
+
"version": "3.0.7",
|
|
6
6
|
"description": "Advanced WhatsApp Web API built on Baileys — interactive messages, buttons, carousels, products, events, payments, polls, albums, sticker packs and more.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"baileys",
|