morpho-socket 0.4.0 → 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 CHANGED
@@ -14,7 +14,7 @@ npm install morpho-socket @whiskeysockets/baileys
14
14
 
15
15
  ## Requirements
16
16
 
17
- - **Node.js ≥ 22.5** (`store-sqlite` and `kv` use the built-in `node:sqlite`)
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
 
@@ -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.5** is required by `store-sqlite` and `kv`. They print an `ExperimentalWarning` from Node itself; it is harmless.
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.4.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.5.0"
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"]
@@ -189,11 +189,14 @@ function createCapture(opts = {}) {
189
189
  return found
190
190
  }
191
191
 
192
- function checkFrame(session, n) {
192
+ const userPart = j => String(j || "").split("@")[0].split(":")[0]
193
+ function checkFrame(session, n, ownLid) {
193
194
  const found = []
194
195
  if (n.tag !== "message") return found
195
196
  const a = n.attrs || {}
196
197
  const sender = a.participant || a.from
198
+ // the account's own LID (messages sent from the phone / another device) never carries a number: not a problem
199
+ if (ownLid && userPart(sender) === ownLid) return found
197
200
  const mode = a.addressing_mode || (isLid(sender) ? "lid" : "pn")
198
201
  if (mode === "lid" && !(a.participant_pn || a.sender_pn || a.peer_recipient_pn) && !isSpecial(sender)) {
199
202
  found.push(alert(session, "frame_lid_without_number", "warn", { jid: maskJid(sender), addressing_mode: a.addressing_mode ?? null }))
@@ -229,12 +232,12 @@ function createCapture(opts = {}) {
229
232
  } catch { stats.errors++ }
230
233
  }
231
234
 
232
- function onFrame(session, node) {
235
+ function onFrame(session, node, sock) {
233
236
  try {
234
237
  if (isBin(node) || !frameTags.includes(node?.tag)) return
235
238
  bump(stats.frames, node.tag)
236
239
  const data = cleanNode(node)
237
- const alerts = checkFrame(session, node)
240
+ const alerts = checkFrame(session, node, userPart(sock?.user?.lid))
238
241
  write({ t: Date.now(), session, kind: "frame", name: node.tag, data, alerts: alerts.length ? alerts.map(a => a.code) : undefined })
239
242
  } catch { stats.errors++ }
240
243
  }
@@ -249,7 +252,7 @@ function createCapture(opts = {}) {
249
252
  undo.push(() => sock.ev.off?.(name, fn))
250
253
  }
251
254
  if (frameTags.length && sock.ws?.on) {
252
- const fn = node => onFrame(sessionId, node)
255
+ const fn = node => onFrame(sessionId, node, sock)
253
256
  sock.ws.on("frame", fn) // every decoded binary node, before Baileys interprets it
254
257
  undo.push(() => sock.ws.off?.("frame", fn))
255
258
  }
@@ -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" }