morpho-socket 0.0.0-stage → 0.2.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/README.md +155 -2
- package/docs/building-a-bot.md +402 -0
- package/package.json +25 -4
- package/packages/commands/README.md +78 -0
- package/packages/commands/antiflood.js +39 -0
- package/packages/commands/index.js +116 -0
- package/packages/commands/package.json +1 -0
- package/packages/context/README.md +97 -0
- package/packages/context/group.js +245 -0
- package/packages/context/index.js +132 -0
- package/packages/context/package.json +1 -0
- package/packages/context/participants.js +12 -0
- package/packages/context/send.js +127 -0
- package/packages/core/README.md +50 -0
- package/packages/core/baileys.js +25 -0
- package/packages/core/codec.js +17 -0
- package/packages/core/index.js +86 -0
- package/packages/core/package.json +7 -0
- package/packages/core/store.js +11 -0
- package/packages/groups/README.md +42 -0
- package/packages/groups/index.js +205 -0
- package/packages/groups/package.json +7 -0
- package/packages/interactive/README.md +41 -0
- package/packages/interactive/index.js +92 -0
- package/packages/interactive/package.json +1 -0
- package/packages/jid/README.md +51 -0
- package/packages/jid/index.js +285 -0
- package/packages/jid/package.json +7 -0
- package/packages/kv/README.md +45 -0
- package/packages/kv/index.js +164 -0
- package/packages/kv/package.json +8 -0
- package/packages/media/README.md +70 -0
- package/packages/media/cache.js +116 -0
- package/packages/media/erros.js +45 -0
- package/packages/media/find.js +51 -0
- package/packages/media/index.js +261 -0
- package/packages/media/package.json +8 -0
- package/packages/message/README.md +53 -0
- package/packages/message/index.js +317 -0
- package/packages/message/package.json +7 -0
- package/packages/pool/README.md +75 -0
- package/packages/pool/index.js +369 -0
- package/packages/pool/package.json +8 -0
- package/packages/presence/README.md +40 -0
- package/packages/presence/index.js +80 -0
- package/packages/presence/package.json +1 -0
- package/packages/reply/README.md +36 -0
- package/packages/reply/index.js +126 -0
- package/packages/reply/package.json +7 -0
- package/packages/reply/voice.js +32 -0
- package/packages/sent/README.md +27 -0
- package/packages/sent/index.js +65 -0
- package/packages/sent/package.json +1 -0
- package/packages/store-file/README.md +28 -0
- package/packages/store-file/index.js +63 -0
- package/packages/store-file/package.json +8 -0
- package/packages/store-sqlite/README.md +25 -0
- package/packages/store-sqlite/index.js +98 -0
- package/packages/store-sqlite/package.json +9 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// @morpho/message
|
|
2
|
+
//
|
|
3
|
+
// Problema que isto resolve: o objeto cru do Baileys (msg.message) tem a mensagem de verdade escondida
|
|
4
|
+
// dentro de "embrulhos" (ephemeralMessage, viewOnceMessageV2, editedMessage...) e cada coisa mora num
|
|
5
|
+
// lugar diferente (texto em .conversation OU .extendedTextMessage.text OU .imageMessage.caption; menções
|
|
6
|
+
// e citação em .contextInfo de qualquer tipo). No Morvyn isso estava refeito em 4 arquivos, cada um
|
|
7
|
+
// com um pedaço da lógica. Aqui vira UMA função: parse(msg) → objeto simples e sempre do mesmo formato.
|
|
8
|
+
//
|
|
9
|
+
// const { parse, findMedia } = require("@morpho/message")
|
|
10
|
+
// const m = parse(msg)
|
|
11
|
+
// m.type // "text" | "image" | "video" | "gif" | "videoNote" | "audio" | "voice" | "sticker" | "document" | ...
|
|
12
|
+
// m.text // texto ou legenda ("" se não tem)
|
|
13
|
+
// m.mentions // jids marcados (em QUALQUER tipo de mensagem, não só texto)
|
|
14
|
+
// m.media // { type, mimetype, seconds, size, ptt, animated, viewOnce, node... } ou null
|
|
15
|
+
// m.quoted // a mensagem citada, já parseada do mesmo jeito (ou null)
|
|
16
|
+
//
|
|
17
|
+
// Nunca lança erro: msg nula, sem descriptografar ou de tipo desconhecido vira type "empty"/"unknown".
|
|
18
|
+
|
|
19
|
+
// "embrulhos": a mensagem de verdade está dentro de .message
|
|
20
|
+
const WRAPPERS = [
|
|
21
|
+
"ephemeralMessage", // chat com mensagens temporárias
|
|
22
|
+
"viewOnceMessage", // visualização única
|
|
23
|
+
"viewOnceMessageV2",
|
|
24
|
+
"viewOnceMessageV2Extension",
|
|
25
|
+
"documentWithCaptionMessage", // documento com legenda
|
|
26
|
+
"editedMessage", // mensagem editada
|
|
27
|
+
"deviceSentMessage", // enviada por outro aparelho do próprio bot
|
|
28
|
+
"groupMentionedMessage",
|
|
29
|
+
"botInvokeMessage",
|
|
30
|
+
"lottieStickerMessage" // figurinha lottie: o stickerMessage está dentro
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
// chaves que não são o conteúdo
|
|
34
|
+
const IGNORE = new Set(["messageContextInfo", "senderKeyDistributionMessage"])
|
|
35
|
+
|
|
36
|
+
// chave do Baileys → tipo simples (função = depende do conteúdo)
|
|
37
|
+
const TYPE_OF_KIND = {
|
|
38
|
+
conversation: "text",
|
|
39
|
+
extendedTextMessage: "text",
|
|
40
|
+
imageMessage: "image",
|
|
41
|
+
videoMessage: n => (n?.gifPlayback ? "gif" : "video"),
|
|
42
|
+
ptvMessage: "videoNote", // vídeo redondo
|
|
43
|
+
audioMessage: n => (n?.ptt ? "voice" : "audio"),
|
|
44
|
+
stickerMessage: "sticker",
|
|
45
|
+
documentMessage: "document",
|
|
46
|
+
contactMessage: "contact",
|
|
47
|
+
contactsArrayMessage: "contact",
|
|
48
|
+
locationMessage: "location",
|
|
49
|
+
liveLocationMessage: "location",
|
|
50
|
+
pollCreationMessage: "poll",
|
|
51
|
+
pollCreationMessageV2: "poll",
|
|
52
|
+
pollCreationMessageV3: "poll",
|
|
53
|
+
pollUpdateMessage: "pollVote",
|
|
54
|
+
reactionMessage: "reaction",
|
|
55
|
+
buttonsMessage: "interactive",
|
|
56
|
+
listMessage: "interactive",
|
|
57
|
+
templateMessage: "interactive",
|
|
58
|
+
interactiveMessage: "interactive",
|
|
59
|
+
buttonsResponseMessage: "reply",
|
|
60
|
+
listResponseMessage: "reply",
|
|
61
|
+
templateButtonReplyMessage: "reply",
|
|
62
|
+
interactiveResponseMessage: "reply",
|
|
63
|
+
groupInviteMessage: "invite",
|
|
64
|
+
eventMessage: "event",
|
|
65
|
+
albumMessage: "album",
|
|
66
|
+
productMessage: "other",
|
|
67
|
+
orderMessage: "other",
|
|
68
|
+
requestPaymentMessage: "other",
|
|
69
|
+
sendPaymentMessage: "other",
|
|
70
|
+
callLogMesssage: "other", // (sic) o nome vem assim do WhatsApp
|
|
71
|
+
newsletterAdminInviteMessage: "other"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// chave do Baileys → tipo de mídia (o mesmo nome que o downloadContentFromMessage espera)
|
|
75
|
+
const MEDIA_OF_KIND = {
|
|
76
|
+
imageMessage: "image",
|
|
77
|
+
videoMessage: "video",
|
|
78
|
+
ptvMessage: "video",
|
|
79
|
+
audioMessage: "audio",
|
|
80
|
+
stickerMessage: "sticker",
|
|
81
|
+
documentMessage: "document"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const MEDIA_TYPES = ["image", "video", "audio", "sticker", "document"]
|
|
85
|
+
|
|
86
|
+
// número, texto, bigint ou Long do protobuf (fileLength, seconds, messageTimestamp...) → number
|
|
87
|
+
function toNumber(v) {
|
|
88
|
+
if (v == null) return 0
|
|
89
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : 0
|
|
90
|
+
if (typeof v === "bigint") return Number(v)
|
|
91
|
+
if (typeof v === "string") { const n = Number(v); return Number.isFinite(n) ? n : 0 }
|
|
92
|
+
if (typeof v === "object") {
|
|
93
|
+
if (typeof v.toNumber === "function") return toNumber(v.toNumber())
|
|
94
|
+
if (typeof v.low === "number" && typeof v.high === "number") return (v.high >>> 0) * 4294967296 + (v.low >>> 0)
|
|
95
|
+
}
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isObj = x => x !== null && typeof x === "object"
|
|
100
|
+
|
|
101
|
+
// tira os embrulhos. { message, viewOnce, edited, ephemeral } — message é o que está dentro (ou null)
|
|
102
|
+
function unwrap(message) {
|
|
103
|
+
let m = isObj(message) ? message : null
|
|
104
|
+
let viewOnce = false, edited = false, ephemeral = false
|
|
105
|
+
for (let i = 0; i < 10 && m; i++) {
|
|
106
|
+
const k = WRAPPERS.find(w => m[w])
|
|
107
|
+
if (!k) break
|
|
108
|
+
if (k.startsWith("viewOnce")) viewOnce = true
|
|
109
|
+
else if (k === "editedMessage") edited = true
|
|
110
|
+
else if (k === "ephemeralMessage") ephemeral = true
|
|
111
|
+
m = isObj(m[k]) && isObj(m[k].message) ? m[k].message : null
|
|
112
|
+
}
|
|
113
|
+
return { message: m, viewOnce, edited, ephemeral }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// acha o conteúdo de verdade. Edição vira o conteúdo NOVO com edited=true; apagar vira { revoked }.
|
|
117
|
+
function resolve(message) {
|
|
118
|
+
let { message: m, viewOnce, edited, ephemeral } = unwrap(message)
|
|
119
|
+
let editTarget = null, revoked = null, protocol = null
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < 3 && m; i++) {
|
|
122
|
+
const kind = Object.keys(m).find(k => !IGNORE.has(k) && m[k] != null)
|
|
123
|
+
if (!kind) return { kind: null, node: null, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
|
|
124
|
+
const node = m[kind]
|
|
125
|
+
|
|
126
|
+
if (kind === "protocolMessage") {
|
|
127
|
+
const t = node?.type
|
|
128
|
+
if ((t === 14 || t === "MESSAGE_EDIT") && isObj(node.editedMessage)) {
|
|
129
|
+
edited = true
|
|
130
|
+
editTarget = node.key || null
|
|
131
|
+
const inner = unwrap(node.editedMessage)
|
|
132
|
+
viewOnce = viewOnce || inner.viewOnce
|
|
133
|
+
ephemeral = ephemeral || inner.ephemeral
|
|
134
|
+
m = inner.message
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if (t === 0 || t === "REVOKE") revoked = node?.key || null
|
|
138
|
+
else if (t === 3 || t === "EPHEMERAL_SETTING") protocol = "ephemeralSetting"
|
|
139
|
+
else protocol = String(t ?? "unknown")
|
|
140
|
+
}
|
|
141
|
+
return { kind, node, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
|
|
142
|
+
}
|
|
143
|
+
return { kind: null, node: null, viewOnce, edited, ephemeral, editTarget, revoked, protocol }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function textOf(kind, node) {
|
|
147
|
+
if (kind === "conversation") return typeof node === "string" ? node : ""
|
|
148
|
+
if (!isObj(node)) return ""
|
|
149
|
+
switch (kind) {
|
|
150
|
+
case "extendedTextMessage": return String(node.text || "")
|
|
151
|
+
case "imageMessage":
|
|
152
|
+
case "videoMessage":
|
|
153
|
+
case "documentMessage": return String(node.caption || "")
|
|
154
|
+
default: return ""
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// resposta de botão / lista / modelo / botão nativo → { id, text } (id null se não deu para ler)
|
|
159
|
+
function selectionOf(kind, node) {
|
|
160
|
+
if (!isObj(node)) return null
|
|
161
|
+
switch (kind) {
|
|
162
|
+
case "buttonsResponseMessage":
|
|
163
|
+
return { id: node.selectedButtonId ?? null, text: String(node.selectedDisplayText || "") }
|
|
164
|
+
case "listResponseMessage":
|
|
165
|
+
return { id: node.singleSelectReply?.selectedRowId ?? null, text: String(node.title || "") }
|
|
166
|
+
case "templateButtonReplyMessage":
|
|
167
|
+
return { id: node.selectedId ?? null, text: String(node.selectedDisplayText || "") }
|
|
168
|
+
case "interactiveResponseMessage": {
|
|
169
|
+
let id = null
|
|
170
|
+
const json = node.nativeFlowResponseMessage?.paramsJson
|
|
171
|
+
if (json) { try { id = JSON.parse(json)?.id ?? null } catch { /* json inválido: sem id */ } }
|
|
172
|
+
return { id, text: String(node.body?.text || "") }
|
|
173
|
+
}
|
|
174
|
+
default: return null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function mediaOf(kind, node, viewOnce) {
|
|
179
|
+
const type = MEDIA_OF_KIND[kind]
|
|
180
|
+
if (!type || !isObj(node)) return null
|
|
181
|
+
const mimetype = String(node.mimetype || "")
|
|
182
|
+
let family = type
|
|
183
|
+
if (type === "document") {
|
|
184
|
+
const f = mimetype.split("/")[0]
|
|
185
|
+
family = f === "image" || f === "video" || f === "audio" ? f : "other"
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
type, // "image" | "video" | "audio" | "sticker" | "document"
|
|
189
|
+
kind, // chave original (ptvMessage continua type "video")
|
|
190
|
+
family, // igual a type, exceto documento: "image"/"video"/"audio" pelo mimetype, ou "other"
|
|
191
|
+
mimetype,
|
|
192
|
+
seconds: toNumber(node.seconds),
|
|
193
|
+
size: toNumber(node.fileLength),
|
|
194
|
+
ptt: !!node.ptt,
|
|
195
|
+
gif: !!node.gifPlayback,
|
|
196
|
+
round: kind === "ptvMessage",
|
|
197
|
+
animated: !!node.isAnimated,
|
|
198
|
+
fileName: node.fileName || null,
|
|
199
|
+
caption: String(node.caption || ""),
|
|
200
|
+
width: toNumber(node.width),
|
|
201
|
+
height: toNumber(node.height),
|
|
202
|
+
viewOnce: !!viewOnce || node.viewOnce === true,
|
|
203
|
+
node // o que vai para o downloadContentFromMessage(node, type)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function lazy(obj, name, fn) {
|
|
208
|
+
let done = false, val
|
|
209
|
+
Object.defineProperty(obj, name, {
|
|
210
|
+
enumerable: true, configurable: true,
|
|
211
|
+
get() { if (!done) { val = fn(); done = true } return val }
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const cache = new WeakMap()
|
|
216
|
+
|
|
217
|
+
function build(msg) {
|
|
218
|
+
const key = isObj(msg?.key) ? msg.key : {}
|
|
219
|
+
const chat = key.remoteJid || null
|
|
220
|
+
const participant = key.participant || null
|
|
221
|
+
const r = resolve(msg?.message)
|
|
222
|
+
|
|
223
|
+
let type
|
|
224
|
+
if (!msg?.message) type = msg?.messageStubType ? "system" : "empty"
|
|
225
|
+
else if (!r.kind) type = "empty"
|
|
226
|
+
else if (r.revoked) type = "revoke"
|
|
227
|
+
else if (r.kind === "protocolMessage") type = "system"
|
|
228
|
+
else {
|
|
229
|
+
const t = TYPE_OF_KIND[r.kind]
|
|
230
|
+
type = typeof t === "function" ? t(r.node) : (t || "unknown")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const node = r.node
|
|
234
|
+
const context = isObj(node) && isObj(node.contextInfo) ? node.contextInfo : null
|
|
235
|
+
const media = mediaOf(r.kind, node, r.viewOnce)
|
|
236
|
+
const reaction = r.kind === "reactionMessage" && isObj(node) ? { emoji: String(node.text || ""), key: node.key || null } : null
|
|
237
|
+
|
|
238
|
+
const m = {
|
|
239
|
+
// quem / onde (ainda crus: o @morpho/jid é que resolve LID → número)
|
|
240
|
+
id: key.id || null,
|
|
241
|
+
chat,
|
|
242
|
+
sender: participant || chat,
|
|
243
|
+
senderAlt: key.participantAlt || key.remoteJidAlt || null,
|
|
244
|
+
fromMe: !!key.fromMe,
|
|
245
|
+
isGroup: !!chat && chat.endsWith("@g.us"),
|
|
246
|
+
isStatus: chat === "status@broadcast",
|
|
247
|
+
isNewsletter: !!chat && chat.endsWith("@newsletter"),
|
|
248
|
+
pushName: msg?.pushName || "",
|
|
249
|
+
timestamp: toNumber(msg?.messageTimestamp),
|
|
250
|
+
key: msg?.key || null,
|
|
251
|
+
|
|
252
|
+
// o que é
|
|
253
|
+
type, // ver lista no topo; "unknown" = chave que o Morpho ainda não conhece (veja m.kind)
|
|
254
|
+
kind: r.kind, // chave original do Baileys, ex.: "imageMessage"
|
|
255
|
+
text: textOf(r.kind, node),
|
|
256
|
+
media,
|
|
257
|
+
selection: r.kind && type === "reply" ? selectionOf(r.kind, node) : null,
|
|
258
|
+
reaction,
|
|
259
|
+
pollKey: r.kind === "pollUpdateMessage" ? (node?.pollCreationMessageKey || null) : null,
|
|
260
|
+
|
|
261
|
+
// detalhes
|
|
262
|
+
mentions: Array.isArray(context?.mentionedJid) ? context.mentionedJid.slice() : [],
|
|
263
|
+
forwarded: !!context?.isForwarded,
|
|
264
|
+
viewOnce: r.viewOnce || (isObj(node) && node.viewOnce === true),
|
|
265
|
+
edited: r.edited,
|
|
266
|
+
editTarget: r.editTarget, // key da mensagem que foi editada (só quando edited)
|
|
267
|
+
revoked: r.revoked, // key da mensagem apagada (só quando type "revoke")
|
|
268
|
+
ephemeral: r.ephemeral, // chegou num chat de mensagens temporárias
|
|
269
|
+
expiration: toNumber(context?.expiration), // segundos até sumir (0 = não some)
|
|
270
|
+
stub: msg?.messageStubType ?? null,
|
|
271
|
+
protocol: r.protocol, // "ephemeralSetting" | código do protocolo | null
|
|
272
|
+
|
|
273
|
+
context, // contextInfo cru
|
|
274
|
+
node // conteúdo cru (string no caso de conversation)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
lazy(m, "quoted", () => {
|
|
278
|
+
if (!context?.quotedMessage) return null
|
|
279
|
+
return parse({
|
|
280
|
+
key: { id: context.stanzaId || null, remoteJid: context.remoteJid || chat, participant: context.participant || undefined },
|
|
281
|
+
message: context.quotedMessage
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
Object.defineProperty(m, "is", { value: (...types) => types.includes(m.type), enumerable: false })
|
|
286
|
+
return m
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Parseia uma mensagem do Baileys (o item de messages.upsert). Parsear a mesma msg de novo devolve o mesmo objeto.
|
|
290
|
+
function parse(msg) {
|
|
291
|
+
if (!isObj(msg)) return build(null)
|
|
292
|
+
let p = cache.get(msg)
|
|
293
|
+
if (!p) { p = build(msg); cache.set(msg, p) }
|
|
294
|
+
return p
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Acha a mídia que o comando deve usar. Por padrão a da mensagem MARCADA tem prioridade (responder uma figurinha
|
|
298
|
+
// com /s), senão a da própria mensagem — igual ao utils/midia.js do Morvyn.
|
|
299
|
+
// types: ["image", "video", ...] (padrão: todos)
|
|
300
|
+
// families: ["image", "video", "audio"] (para pegar também documento que é, na verdade, imagem/vídeo/áudio)
|
|
301
|
+
// quoted: "prefer" (padrão) | "only" | "never"
|
|
302
|
+
// Devolve a mídia (ver m.media) com fromQuoted: true/false, ou null.
|
|
303
|
+
// accept: (media) => boolean filtro extra (ex.: ignorar documento que não é imagem/vídeo/áudio)
|
|
304
|
+
function findMedia(msg, { types, families, quoted = "prefer", accept } = {}) {
|
|
305
|
+
const m = parse(msg)
|
|
306
|
+
const q = quoted === "never" ? null : m.quoted?.media || null
|
|
307
|
+
const own = quoted === "only" ? null : m.media
|
|
308
|
+
const ok = a => a && (!types || types.includes(a.type)) && (!families || families.includes(a.family)) && (!accept || accept(a))
|
|
309
|
+
if (ok(q)) return { ...q, fromQuoted: true }
|
|
310
|
+
if (ok(own)) return { ...own, fromQuoted: false }
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// atalho: só o texto (ou legenda) de uma mensagem
|
|
315
|
+
const textOfMessage = msg => parse(msg).text
|
|
316
|
+
|
|
317
|
+
module.exports = { parse, findMedia, unwrap, textOf: textOfMessage, toNumber, WRAPPERS, MEDIA_TYPES }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@morpho/message",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mensagem do WhatsApp em formato simples no Morpho: desembrulha (visualização única, temporária, editada...), descobre o tipo, texto/legenda, menções, mídia, resposta de botão/lista e a mensagem citada. Sem dependências. Junta o que no Morvyn estava espalhado em tipoMensagem.js, midia.js, global.js e handler.js.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"license": "MIT"
|
|
7
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# morpho-socket/pool
|
|
2
|
+
|
|
3
|
+
Runs **several WhatsApp numbers in one process**: connect, pair by code, reconnect with backoff, stop, reset. No business rules (plans, limits, who is the owner) — those plug in through events.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { MorphoPool } = require("morpho-socket/pool")
|
|
7
|
+
const { initAuthCreds } = require("@whiskeysockets/baileys")
|
|
8
|
+
|
|
9
|
+
const pool = new MorphoPool({ store, initCreds: initAuthCreds, maxAttemptsWithoutPairing: 8 })
|
|
10
|
+
|
|
11
|
+
pool.on("socket", (id, sock, { listen }) => listen("messages.upsert", data => handle(id, sock, data)))
|
|
12
|
+
pool.on("pairing-code", (id, code) => console.log(id, "→", code))
|
|
13
|
+
pool.on("connect", (id, sock) => { /* socket is open */ })
|
|
14
|
+
|
|
15
|
+
await pool.startAll(["client-a", { sessionId: "client-b", phoneNumber: "5511999999999" }], { intervalMs: 1500 })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Pairing
|
|
19
|
+
|
|
20
|
+
If a session has no saved credentials and you pass `phoneNumber`, the pool requests a pairing code and emits `pairing-code`. The code is requested only after WhatsApp answers the handshake (the `qr` event) — asking earlier makes the server reject the session. A session that started pairing but never finished is discarded on the next connect, so a failed attempt does not leave the number stuck in a 401 loop.
|
|
21
|
+
|
|
22
|
+
## What happens when the connection drops
|
|
23
|
+
|
|
24
|
+
| Code | Reason | What the pool does | Extra event |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| 401 | logged out | **stops** (always) | `logged-out` |
|
|
27
|
+
| 440 | replaced: another instance opened the same session | **stops** (reconnecting would start a fight) | `replaced` |
|
|
28
|
+
| 403 | forbidden: number restricted or banned | **stops** | `forbidden` |
|
|
29
|
+
| 515 | restart required (normal right after pairing) | reconnects **immediately** (up to 3 in a row, then backoff) | — |
|
|
30
|
+
| anything else | network drop, timeout… | reconnects with backoff (2 s … 60 s) | `reconnecting` |
|
|
31
|
+
|
|
32
|
+
Every terminal stop also emits `stopped` with `{ reason }`, and rejects the pending `start()` if the session never opened. Override per code with `policy`:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
new MorphoPool({ store, initCreds, policy: { 440: "reconnect", 515: "immediate" } }) // "reconnect" | "immediate" | "stop"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
**Constructor options**
|
|
41
|
+
|
|
42
|
+
| Option | Meaning |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `store`, `initCreds` | required — a [`MorphoAuthStore`](../core) and Baileys' `initAuthCreds` |
|
|
45
|
+
| `socketConfig(sessionId, state)` | extra `makeWASocket` options (`getMessage`, `cachedGroupMetadata`, `browser`…) merged into the default factory |
|
|
46
|
+
| `makeSocket({ sessionId, state, saveCreds })` | replace the socket factory entirely (also how you test without WhatsApp). `socketConfig` is only used by the default factory |
|
|
47
|
+
| `backoff(attempt)` | milliseconds to wait; default exponential, capped at 60 s |
|
|
48
|
+
| `policy` | per-close-code action (see above). 401 always stops |
|
|
49
|
+
| `maxAttemptsWithoutPairing` | a number that was **never paired** gives up after N consecutive drops and emits `gave-up`. A paired session never gives up. Default `null` (try forever) |
|
|
50
|
+
| `skipNetworkVersion`, `versionTimeoutMs` | the protocol version is fetched once and cached for 1 h, with a timeout; if the lookup fails the last good version (or Baileys' bundled one) is used and a warning is printed |
|
|
51
|
+
|
|
52
|
+
**Methods**
|
|
53
|
+
|
|
54
|
+
| | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `start(id, { phoneNumber })` | connects; resolves the first time the connection opens |
|
|
57
|
+
| `startAll(items, { intervalMs })` | starts in sequence with a gap instead of opening every connection at once; does not wait for pairing. Items: `"id"`, `{ sessionId, phoneNumber }` or `{ sessionId, opts }` |
|
|
58
|
+
| `stop(id)` | disconnects but **keeps** the saved session (`start()` again, no pairing) |
|
|
59
|
+
| `reset(id)` | disconnects **and deletes** the saved session (next `start()` asks for pairing) |
|
|
60
|
+
| `get(id)` | the current socket, or `null` |
|
|
61
|
+
| `status(id)` / `info(id)` / `list()` | `"connecting" \| "open" \| "reconnecting" \| "stopped"` · `{ status, attempts, paired, stopReason }` · all sessions |
|
|
62
|
+
|
|
63
|
+
**Events**
|
|
64
|
+
|
|
65
|
+
| Event | Arguments |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `socket` | `(id, sock, { listen })` — one per (re)connection. `listen` is `sock.ev.on` that ignores events from a replaced/stopped socket and routes listener errors (sync or async) to `error` |
|
|
68
|
+
| `connect` | `(id, sock)` |
|
|
69
|
+
| `pairing-code` | `(id, code)` |
|
|
70
|
+
| `disconnect` | `(id, { code, message, loggedOut, reason, action })` |
|
|
71
|
+
| `reconnecting` | `(id, { attempt, delayMs, reason, immediate })` |
|
|
72
|
+
| `logged-out` · `replaced` · `forbidden` · `gave-up` · `stopped` | `(id, info)` |
|
|
73
|
+
| `error` | `(id, error)` — without a listener the error is printed instead of crashing the process |
|
|
74
|
+
|
|
75
|
+
Also exported: `STATUS`, `CLOSE_REASONS`, `DEFAULT_POLICY`.
|