morpho-socket 0.4.1 → 0.5.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 +3 -1
- package/docs/building-a-bot.md +1 -1
- package/package.json +4 -3
- package/packages/capture/index.js +1 -1
- package/packages/events/README.md +53 -0
- package/packages/events/index.js +280 -0
- package/packages/events/package.json +1 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install morpho-socket @whiskeysockets/baileys
|
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
- **Node.js ≥ 22.
|
|
17
|
+
- **Node.js ≥ 22.12** (Baileys 7 is ESM and Morpho loads it with `require`, which needs 22.12+; `store-sqlite` and `kv` use the built-in `node:sqlite`)
|
|
18
18
|
- **Baileys** installed in your own project (Morpho loads it from there; it is not bundled)
|
|
19
19
|
- **ffmpeg** on the `PATH`, only if you send voice notes from non-Opus audio
|
|
20
20
|
|
|
@@ -41,6 +41,8 @@ Morpho is CommonJS.
|
|
|
41
41
|
| Relations between two people (couple, friend, parent, team…) with limits | [`links`](packages/links) |
|
|
42
42
|
| Do something later or on repeat (reminders, daily open/close), surviving restarts | [`scheduler`](packages/scheduler) |
|
|
43
43
|
| Conversations of several messages: confirmations, forms, games, reply-to-answer | [`ask`](packages/ask) |
|
|
44
|
+
| Baileys changes a field or a payload and every layer above breaks | [`events`](packages/events) — Morpho's own events (`message`, `member.*`) with a stable shape |
|
|
45
|
+
| "What is WhatsApp actually sending me?" (LID without number, new fields) | [`capture`](packages/capture) — masked capture, alerts and fixtures |
|
|
44
46
|
|
|
45
47
|
## A quick look
|
|
46
48
|
|
package/docs/building-a-bot.md
CHANGED
|
@@ -399,4 +399,4 @@ my-bot/
|
|
|
399
399
|
- **Spread out startup.** `pool.startAll(items, { intervalMs })` opens connections one after another rather than all at once.
|
|
400
400
|
- **Always give interactive messages a `fallback`.**
|
|
401
401
|
- **ffmpeg** must be on the `PATH` to send voice notes from non-Opus audio.
|
|
402
|
-
- **Node ≥ 22.
|
|
402
|
+
- **Node ≥ 22.12** is required: Baileys 7 is ESM and Morpho loads it with `require`. `store-sqlite` and `kv` also need `node:sqlite` (22.5+). They print an `ExperimentalWarning` from Node itself; it is harmless.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpho-socket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Blocos para bots de WhatsApp sobre o Baileys",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=22.
|
|
7
|
+
"node": ">=22.12.0"
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
10
|
"@whiskeysockets/baileys": "*"
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"./links": "./packages/links/index.js",
|
|
35
35
|
"./scheduler": "./packages/scheduler/index.js",
|
|
36
36
|
"./ask": "./packages/ask/index.js",
|
|
37
|
-
"./capture": "./packages/capture/index.js"
|
|
37
|
+
"./capture": "./packages/capture/index.js",
|
|
38
|
+
"./events": "./packages/events/index.js"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"test": "node test/run-all.js"
|
|
@@ -21,7 +21,7 @@ const path = require("node:path")
|
|
|
21
21
|
const crypto = require("node:crypto")
|
|
22
22
|
|
|
23
23
|
const EVENTS = [
|
|
24
|
-
"messages.upsert", "messages.update", "lid-mapping.update", "group-participants.update", "groups.upsert",
|
|
24
|
+
"messages.upsert", "messages.update", "messages.reaction", "messages.delete", "group.join-request", "lid-mapping.update", "group-participants.update", "groups.upsert",
|
|
25
25
|
"groups.update", "contacts.upsert", "contacts.update", "connection.update", "call"
|
|
26
26
|
]
|
|
27
27
|
const FRAME_TAGS = ["message", "notification", "call"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# morpho-socket/events
|
|
2
|
+
|
|
3
|
+
Morpho's **own events**, with a shape that does not change when Baileys or WhatsApp do. The Baileys adapter (7.0.0-rc14) lives in one file; everything above listens to Morpho's events, not to Baileys'.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const { createEvents } = require("morpho-socket/events")
|
|
7
|
+
const events = createEvents({ identity }) // identity = createIdentity() from morpho-socket/jid (recommended)
|
|
8
|
+
events.attachPool(pool) // or events.attach(sock, sessionId)
|
|
9
|
+
|
|
10
|
+
events.on("message", m => {
|
|
11
|
+
m.chatKind // "group" | "private" | "status" | "broadcast" | "newsletter"
|
|
12
|
+
m.source // "notify" (new) | "append" (history, or sent from the phone)
|
|
13
|
+
m.fromMe, m.id, m.chatId, m.addressingMode
|
|
14
|
+
m.sender // { jid, lid, pn, number, formatted, resolved, isBot, source } <- already resolved
|
|
15
|
+
m.content // parse() from morpho-socket/message
|
|
16
|
+
m.raw, m.sock, m.session
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
events.on("member.join", e => { e.chatId; e.member; e.by })
|
|
20
|
+
events.on("member.leave", e => { e.kicked }) // true: someone else removed them; false: they left
|
|
21
|
+
// also member.promote, member.demote, member.update (anything else Baileys reports)
|
|
22
|
+
|
|
23
|
+
events.on("reaction", e => { e.emoji; e.removed; e.messageId; e.by }) // e.by = who reacted
|
|
24
|
+
events.on("message.edit", e => { e.messageId; e.content; e.by }) // content parsed like a normal message
|
|
25
|
+
events.on("message.delete", e => { e.messageId; e.by }) // by = null when it comes from history
|
|
26
|
+
events.on("chat.clear", e => { e.chatId })
|
|
27
|
+
events.on("receipt", e => { e.status; e.user }) // "delivered" | "read" | "played"
|
|
28
|
+
events.on("call", e => { e.status; e.from; e.isVideo; e.groupId })
|
|
29
|
+
events.on("connection", e => { e.state; e.error; e.online; e.hasQr }) // partial updates; the QR itself is never delivered
|
|
30
|
+
events.on("group.update", e => { e.changes; e.by }) // { subject, description, onlyAdminsSend, onlyAdminsEdit, membersCanAdd, joinApproval, ephemeralDuration }
|
|
31
|
+
events.on("group.new", e => { e.subject; e.size }) // the bot entered or created a group
|
|
32
|
+
events.on("group.join-request", e => { e.action; e.method; e.member; e.by })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`member` and `by` are `{ jid, lid, pn, number, formatted, resolved, source, admin }`, whether Baileys sent participants as strings (6) or as objects with `lid` / `phoneNumber` (7).
|
|
36
|
+
|
|
37
|
+
## Behavior
|
|
38
|
+
|
|
39
|
+
- Events keep the order Baileys delivered them (an `add` and a later `remove` of the same person never swap), but listeners run without holding the next event: a slow command does not delay the next message.
|
|
40
|
+
- A listener that throws never stops the other listeners or the bot: the error goes to `onError` and is counted in `events.stats.listenerErrors`. The adapter itself never throws into Baileys (`stats.adapterErrors`).
|
|
41
|
+
- `events.stats.emitted` counts what was emitted per event name.
|
|
42
|
+
- Nothing here touches the network. Number resolution uses what `jid` already knows (`participantAlt`, `phoneNumber`, the LID map).
|
|
43
|
+
|
|
44
|
+
## Why
|
|
45
|
+
|
|
46
|
+
The first time WhatsApp moved people to `@lid`, the sender moved from one field to another and each package had to learn that by itself. Here the difference is absorbed once. Use `morpho-socket/capture` to see what Baileys really delivers and to build fixtures for this adapter.
|
|
47
|
+
|
|
48
|
+
- **No listener, no work.** An adapter that nobody listens to does nothing (receipts and `messages.update` are frequent and cost an identity lookup each).
|
|
49
|
+
- In `messages.update`, only revoke (delete) and edit become events. Acks, poll votes and event responses are counted in `stats.ignored` and left to the packages that already handle them (`sent`).
|
|
50
|
+
|
|
51
|
+
## Not covered yet
|
|
52
|
+
|
|
53
|
+
Presence, chats/contacts sync, blocklist, labels, newsletters, history sync and `creds.update` still come from Baileys directly (the pool already owns `creds.update`). They enter the same way: one adapter function and one test.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// @morpho/events
|
|
2
|
+
//
|
|
3
|
+
// Morpho's OWN events, with a shape that does not depend on the Baileys version. The Baileys adapter lives in this one
|
|
4
|
+
// file: when WhatsApp or Baileys change something (the move to @lid, a field that moved, a payload that went from
|
|
5
|
+
// strings to objects), the fix goes here and nothing above notices.
|
|
6
|
+
//
|
|
7
|
+
// const { createEvents } = require("morpho-socket/events")
|
|
8
|
+
// const events = createEvents({ identity }) // identity: morpho-socket/jid (optional, but without it numbers are not resolved)
|
|
9
|
+
// events.attachPool(pool) // or events.attach(sock, sessionId)
|
|
10
|
+
//
|
|
11
|
+
// events.on("message", m => {
|
|
12
|
+
// m.id m.chatId m.chatKind // "group" | "private" | "status" | "broadcast" | "newsletter"
|
|
13
|
+
// m.fromMe m.source // source: "notify" (new) | "append" (history / sent from the phone)
|
|
14
|
+
// m.sender // { jid, lid, pn, number, formatted, resolved, isBot, source }
|
|
15
|
+
// m.addressingMode // "lid" | "pn" | null
|
|
16
|
+
// m.content // morpho-socket/message parse(): type, text, mentions, media, quoted...
|
|
17
|
+
// m.raw m.sock m.session // the Baileys message, the socket and the session id, when you need them
|
|
18
|
+
// })
|
|
19
|
+
// events.on("member.join" | "member.leave" | "member.promote" | "member.demote" | "member.update", e => {
|
|
20
|
+
// e.chatId e.member e.by e.action // member/by: { jid, lid, pn, number, formatted, resolved, source, admin }
|
|
21
|
+
// e.kicked // member.leave only: true when someone else removed them
|
|
22
|
+
// })
|
|
23
|
+
//
|
|
24
|
+
// events.on("reaction", e => e.emoji, e.removed, e.messageId, e.by)
|
|
25
|
+
// events.on("message.edit", e => e.messageId, e.content (parsed new content), e.by)
|
|
26
|
+
// events.on("message.delete", e => e.messageId, e.by) // `by` is null when it comes from history
|
|
27
|
+
// events.on("receipt", e => e.status) // "delivered" | "read" | "played", e.user
|
|
28
|
+
// events.on("call", e => e.status, e.from, e.isVideo, e.groupId)
|
|
29
|
+
// events.on("connection", e => e.state, e.error, e.online) // partial updates: only what changed is filled
|
|
30
|
+
// events.on("group.update" | "group.new" | "group.join-request", ...)
|
|
31
|
+
//
|
|
32
|
+
// Rules: a listener that throws never stops the others or the bot (it is counted in stats and sent to onError);
|
|
33
|
+
// the adapter never throws into Baileys; nothing here talks to the network.
|
|
34
|
+
|
|
35
|
+
const { kind, normalize, digits, formatNumber } = require("../jid")
|
|
36
|
+
|
|
37
|
+
const lazyParse = () => require("../message").parse
|
|
38
|
+
const lazyToNumber = () => require("../message").toNumber
|
|
39
|
+
const REVOKE = 1 // WAMessageStubType.REVOKE in Baileys 7
|
|
40
|
+
|
|
41
|
+
function chatKindOf(chatId) {
|
|
42
|
+
if (chatId === "status@broadcast") return "status"
|
|
43
|
+
const k = kind(chatId)
|
|
44
|
+
if (k === "group") return "group"
|
|
45
|
+
if (k === "broadcast") return "broadcast"
|
|
46
|
+
if (k === "newsletter") return "newsletter"
|
|
47
|
+
return "private"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createEvents({ identity, parse = lazyParse(), toNumber = lazyToNumber(), onError = (e, where) => console.error(`[events] ${where}:`, e) } = {}) {
|
|
51
|
+
const listeners = new Map()
|
|
52
|
+
const stats = { emitted: {}, ignored: {}, listenerErrors: 0, adapterErrors: 0 }
|
|
53
|
+
const bump = (o, k) => { o[k] = (o[k] || 0) + 1 }
|
|
54
|
+
|
|
55
|
+
function on(name, fn) {
|
|
56
|
+
if (typeof fn !== "function") throw new TypeError("events.on: pass a function")
|
|
57
|
+
if (!listeners.has(name)) listeners.set(name, new Set())
|
|
58
|
+
listeners.get(name).add(fn)
|
|
59
|
+
return () => listeners.get(name)?.delete(fn)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function emit(name, payload) {
|
|
63
|
+
bump(stats.emitted, name)
|
|
64
|
+
for (const fn of [...(listeners.get(name) || [])]) {
|
|
65
|
+
try { await fn(payload) } catch (e) { stats.listenerErrors++; try { onError(e, name) } catch { /* even the error handler must not break the bot */ } }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- identity helpers ----
|
|
70
|
+
function basicSender(raw, sock) {
|
|
71
|
+
const k = raw?.key || {}
|
|
72
|
+
const jid = k.fromMe ? sock?.user?.id : (kind(k.remoteJid) === "group" ? k.participant : k.remoteJid)
|
|
73
|
+
const lid = kind(jid) === "lid" ? normalize(jid) : null
|
|
74
|
+
const alt = kind(jid) === "group" ? null : (kind(k.remoteJid) === "group" ? k.participantAlt : k.remoteJidAlt)
|
|
75
|
+
const number = digits(jid || "") || digits(alt || "")
|
|
76
|
+
return { jid: jid || null, lid, pn: number ? `${number}@s.whatsapp.net` : null, number, formatted: formatNumber(number), resolved: !!number, isBot: !!k.fromMe, source: number ? "message" : null, group: null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function senderOf(raw, sock, session) {
|
|
80
|
+
if (!identity) return basicSender(raw, sock)
|
|
81
|
+
return identity.resolveSender(raw, sock, session) // never throws (falls back to what came in the message)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// a participant/author as Baileys 6 (string) or 7 (object {id, lid, phoneNumber, admin}) delivers it
|
|
85
|
+
async function person(p, { sock, session, group, pn: pnHint } = {}) {
|
|
86
|
+
const o = typeof p === "string" ? { id: p } : (p || {})
|
|
87
|
+
const id = o.id || o.jid || null
|
|
88
|
+
const lid = o.lid ? normalize(o.lid) : (kind(id) === "lid" ? normalize(id) : null)
|
|
89
|
+
let pn = [o.phoneNumber, o.pn, pnHint, kind(id) === "pn" ? id : null].find(x => kind(x) === "pn") || null
|
|
90
|
+
let number = digits(pn || ""), source = number ? (kind(id) === "pn" ? "message" : "alt") : null
|
|
91
|
+
if (!number && lid && identity) {
|
|
92
|
+
const r = await identity.resolveJid(lid, { sock, clientId: session, group }).catch(() => null)
|
|
93
|
+
if (r?.number) { number = r.number; source = r.source }
|
|
94
|
+
}
|
|
95
|
+
if (number && !pn) pn = `${number}@s.whatsapp.net`
|
|
96
|
+
const admin = o.admin ?? (o.isSuperAdmin ? "superadmin" : o.isAdmin ? "admin" : null)
|
|
97
|
+
return { jid: normalize(id), lid, pn: pn ? normalize(pn) : null, number, formatted: formatNumber(number), resolved: !!number, source, admin }
|
|
98
|
+
}
|
|
99
|
+
const samePerson = (a, b) => {
|
|
100
|
+
const ids = x => [x?.jid, x?.lid, x?.pn].filter(Boolean).map(normalize)
|
|
101
|
+
const bs = new Set(ids(b))
|
|
102
|
+
return ids(a).some(x => bs.has(x))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- adapters (Baileys 7.0.0-rc14) ----
|
|
106
|
+
async function onUpsert(sock, session, data) {
|
|
107
|
+
const messages = Array.isArray(data?.messages) ? data.messages : []
|
|
108
|
+
for (const raw of messages) {
|
|
109
|
+
try {
|
|
110
|
+
const k = raw?.key || {}
|
|
111
|
+
const content = parse(raw)
|
|
112
|
+
const m = {
|
|
113
|
+
session, sock, raw,
|
|
114
|
+
id: k.id || null,
|
|
115
|
+
chatId: k.remoteJid || null,
|
|
116
|
+
chatKind: chatKindOf(k.remoteJid),
|
|
117
|
+
fromMe: !!k.fromMe,
|
|
118
|
+
source: data?.type === "append" ? "append" : (data?.type || "notify"),
|
|
119
|
+
addressingMode: k.addressingMode || null,
|
|
120
|
+
ts: content?.timestamp || null,
|
|
121
|
+
sender: await senderOf(raw, sock, session),
|
|
122
|
+
content
|
|
123
|
+
}
|
|
124
|
+
emit("message", m) // not awaited: a slow listener (a command that downloads something) must not hold the next message
|
|
125
|
+
} catch (e) { stats.adapterErrors++; try { onError(e, "adapter:messages.upsert") } catch { /* swallowed */ } }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const MEMBER_EVENT = { add: "member.join", remove: "member.leave", promote: "member.promote", demote: "member.demote" }
|
|
130
|
+
async function onParticipants(sock, session, u) {
|
|
131
|
+
try {
|
|
132
|
+
if (!u || !Array.isArray(u.participants)) return
|
|
133
|
+
const group = u.id
|
|
134
|
+
const by = u.author ? await person({ id: u.author }, { sock, session, group, pn: u.authorPn }) : null
|
|
135
|
+
for (const p of u.participants) {
|
|
136
|
+
const member = await person(p, { sock, session, group })
|
|
137
|
+
const name = MEMBER_EVENT[u.action] || "member.update"
|
|
138
|
+
const e = { session, sock, chatId: group, action: u.action, member, by, raw: u }
|
|
139
|
+
if (name === "member.leave") e.kicked = !!by && !samePerson(by, member)
|
|
140
|
+
emit(name, e)
|
|
141
|
+
}
|
|
142
|
+
} catch (e) { stats.adapterErrors++; try { onError(e, "adapter:group-participants.update") } catch { /* swallowed */ } }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
const wanted = (...names) => names.some(n => listeners.get(n)?.size)
|
|
147
|
+
const safe = async (where, fn) => {
|
|
148
|
+
try { await fn() } catch (e) { stats.adapterErrors++; try { onError(e, `adapter:${where}`) } catch { /* swallowed */ } }
|
|
149
|
+
}
|
|
150
|
+
const base = (sock, session) => ({ session, sock })
|
|
151
|
+
const whoKey = (key, sock, session) => senderOf({ key }, sock, session)
|
|
152
|
+
const seconds = v => { const n = toNumber(v); return n ? (n > 1e11 ? Math.floor(n / 1000) : n) : null }
|
|
153
|
+
|
|
154
|
+
async function onReaction(sock, session, list) {
|
|
155
|
+
if (!wanted("reaction")) return
|
|
156
|
+
await safe("messages.reaction", async () => {
|
|
157
|
+
for (const { key, reaction } of Array.isArray(list) ? list : []) {
|
|
158
|
+
const by = reaction?.key ? await whoKey(reaction.key, sock, session) : null
|
|
159
|
+
emit("reaction", {
|
|
160
|
+
...base(sock, session), chatId: key?.remoteJid || null, chatKind: chatKindOf(key?.remoteJid), messageId: key?.id || null,
|
|
161
|
+
targetFromMe: !!key?.fromMe, by, emoji: reaction?.text || "", removed: !reaction?.text, ts: seconds(reaction?.senderTimestampMs), raw: { key, reaction }
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// messages.update carries many things (acks, poll votes...). Only revoke and edit mean something to a bot for now.
|
|
168
|
+
async function onMessagesUpdate(sock, session, list) {
|
|
169
|
+
if (!wanted("message.delete", "message.edit")) return
|
|
170
|
+
await safe("messages.update", async () => {
|
|
171
|
+
for (const u of Array.isArray(list) ? list : []) {
|
|
172
|
+
const key = u?.key || {}, up = u?.update || {}
|
|
173
|
+
if (up.messageStubType === REVOKE) {
|
|
174
|
+
emit("message.delete", { ...base(sock, session), chatId: key.remoteJid || null, chatKind: chatKindOf(key.remoteJid), messageId: key.id || null, by: await whoKey(key, sock, session), raw: u })
|
|
175
|
+
} else if (up.message?.editedMessage) {
|
|
176
|
+
const content = parse({ key, message: up.message, messageTimestamp: up.messageTimestamp })
|
|
177
|
+
emit("message.edit", { ...base(sock, session), chatId: key.remoteJid || null, chatKind: chatKindOf(key.remoteJid), messageId: key.id || null, by: await whoKey(key, sock, session), content, ts: seconds(up.messageTimestamp), raw: u })
|
|
178
|
+
} else bump(stats.ignored, "messages.update")
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function onMessagesDelete(sock, session, d) {
|
|
184
|
+
if (!wanted("message.delete", "chat.clear")) return
|
|
185
|
+
await safe("messages.delete", async () => {
|
|
186
|
+
if (d?.all && d.jid) return emit("chat.clear", { ...base(sock, session), chatId: d.jid, chatKind: chatKindOf(d.jid), raw: d })
|
|
187
|
+
for (const key of Array.isArray(d?.keys) ? d.keys : [])
|
|
188
|
+
emit("message.delete", { ...base(sock, session), chatId: key.remoteJid || null, chatKind: chatKindOf(key.remoteJid), messageId: key.id || null, by: null, raw: d })
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function onReceipt(sock, session, list) {
|
|
193
|
+
if (!wanted("receipt")) return
|
|
194
|
+
await safe("message-receipt.update", async () => {
|
|
195
|
+
for (const { key, receipt } of Array.isArray(list) ? list : []) {
|
|
196
|
+
const status = receipt?.playedTimestamp ? "played" : receipt?.readTimestamp ? "read" : "delivered"
|
|
197
|
+
emit("receipt", { ...base(sock, session), chatId: key?.remoteJid || null, chatKind: chatKindOf(key?.remoteJid), messageId: key?.id || null, status, user: receipt?.userJid ? await person({ id: receipt.userJid }, { sock, session, group: kind(key?.remoteJid) === "group" ? key.remoteJid : undefined }) : null, raw: { key, receipt } })
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function onCall(sock, session, list) {
|
|
203
|
+
if (!wanted("call")) return
|
|
204
|
+
await safe("call", async () => {
|
|
205
|
+
for (const c of Array.isArray(list) ? list : []) {
|
|
206
|
+
emit("call", { ...base(sock, session), id: c.id || null, status: c.status || null, chatId: c.chatId || null, from: c.from ? await person({ id: c.from }, { sock, session, pn: c.callerPn }) : null,
|
|
207
|
+
isVideo: !!c.isVideo, isGroup: !!c.isGroup, groupId: c.groupJid || null, date: c.date || null, offline: !!c.offline, raw: c })
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function onConnection(sock, session, u) {
|
|
213
|
+
if (!wanted("connection")) return
|
|
214
|
+
await safe("connection.update", async () => {
|
|
215
|
+
const err = u?.lastDisconnect?.error
|
|
216
|
+
emit("connection", { ...base(sock, session), state: u?.connection || null, isNewLogin: u?.isNewLogin ?? null, hasQr: !!u?.qr, online: u?.isOnline ?? null, receivedPendingNotifications: u?.receivedPendingNotifications ?? null,
|
|
217
|
+
error: err ? { code: err.output?.statusCode ?? err.statusCode ?? null, message: err.message ?? null } : null })
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const GROUP_FIELDS = { subject: "subject", desc: "description", announce: "onlyAdminsSend", restrict: "onlyAdminsEdit", memberAddMode: "membersCanAdd", joinApprovalMode: "joinApproval", ephemeralDuration: "ephemeralDuration" }
|
|
222
|
+
async function onGroupsUpdate(sock, session, list) {
|
|
223
|
+
if (!wanted("group.update")) return
|
|
224
|
+
await safe("groups.update", async () => {
|
|
225
|
+
for (const u of Array.isArray(list) ? list : []) {
|
|
226
|
+
const changes = {}
|
|
227
|
+
for (const [from, to] of Object.entries(GROUP_FIELDS)) if (u?.[from] !== undefined) changes[to] = u[from]
|
|
228
|
+
const by = u?.author ? await person({ id: u.author }, { sock, session, group: u.id, pn: u.authorPn }) : null
|
|
229
|
+
emit("group.update", { ...base(sock, session), chatId: u?.id || null, changes, by, raw: u })
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
async function onGroupsUpsert(sock, session, list) {
|
|
234
|
+
if (!wanted("group.new")) return
|
|
235
|
+
await safe("groups.upsert", async () => {
|
|
236
|
+
for (const g of Array.isArray(list) ? list : [])
|
|
237
|
+
emit("group.new", { ...base(sock, session), chatId: g?.id || null, subject: g?.subject ?? null, size: g?.size ?? g?.participants?.length ?? null, addressingMode: g?.addressingMode || null, raw: g })
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
async function onJoinRequest(sock, session, r) {
|
|
241
|
+
if (!wanted("group.join-request")) return
|
|
242
|
+
await safe("group.join-request", async () => {
|
|
243
|
+
if (!r) return
|
|
244
|
+
emit("group.join-request", { ...base(sock, session), chatId: r.id || null, action: r.action || null, method: r.method || null,
|
|
245
|
+
member: await person({ id: r.participant }, { sock, session, group: r.id, pn: r.participantPn }),
|
|
246
|
+
by: r.author ? await person({ id: r.author }, { sock, session, group: r.id, pn: r.authorPn }) : null, raw: r })
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function attach(sock, session = "default", { listen } = {}) {
|
|
251
|
+
const undo = []
|
|
252
|
+
// Building a payload is async (it resolves who is who). The queue keeps events in the order Baileys delivered them
|
|
253
|
+
// (add → remove of the same person); listeners themselves run without holding the queue.
|
|
254
|
+
let queue = Promise.resolve()
|
|
255
|
+
const ordered = fn => d => { queue = queue.then(() => fn(d)).catch(() => { stats.adapterErrors++ }) }
|
|
256
|
+
const add = (ev, fn) => {
|
|
257
|
+
if (listen) return listen(ev, fn) // the pool's `listen` already ignores events from a replaced socket
|
|
258
|
+
sock.ev.on(ev, fn); undo.push(() => sock.ev.off?.(ev, fn))
|
|
259
|
+
}
|
|
260
|
+
add("messages.upsert", ordered(d => onUpsert(sock, session, d)))
|
|
261
|
+
add("group-participants.update", ordered(d => onParticipants(sock, session, d)))
|
|
262
|
+
add("messages.reaction", ordered(d => onReaction(sock, session, d)))
|
|
263
|
+
add("messages.update", ordered(d => onMessagesUpdate(sock, session, d)))
|
|
264
|
+
add("messages.delete", ordered(d => onMessagesDelete(sock, session, d)))
|
|
265
|
+
add("message-receipt.update", ordered(d => onReceipt(sock, session, d)))
|
|
266
|
+
add("call", ordered(d => onCall(sock, session, d)))
|
|
267
|
+
add("connection.update", ordered(d => onConnection(sock, session, d)))
|
|
268
|
+
add("groups.update", ordered(d => onGroupsUpdate(sock, session, d)))
|
|
269
|
+
add("groups.upsert", ordered(d => onGroupsUpsert(sock, session, d)))
|
|
270
|
+
add("group.join-request", ordered(d => onJoinRequest(sock, session, d)))
|
|
271
|
+
return () => { for (const f of undo) try { f() } catch { /* socket already gone */ } }
|
|
272
|
+
}
|
|
273
|
+
function attachPool(pool) {
|
|
274
|
+
pool.on("socket", (session, sock, { listen } = {}) => { attach(sock, session, { listen }) })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { on, attach, attachPool, stats, chatKindOf }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { createEvents, chatKindOf }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "name": "@morpho/events", "version": "0.1.0", "description": "Morpho's own events with a stable shape (message, member.*) on top of the Baileys adapter.", "main": "index.js", "license": "MIT" }
|