@stone-wpp/ofc 1.0.5 → 1.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 +48 -30
- package/lib/Signal/lid-mapping.js +9 -1
- package/lib/Socket/group-protection.d.ts +146 -0
- package/lib/Socket/group-protection.js +571 -0
- package/lib/Socket/index.d.ts +1 -1
- package/lib/Socket/index.js +3 -1
- package/lib/Socket/messages-recv.js +47 -28
- package/lib/Socket/messages-send.js +10 -5
- package/lib/Socket/socket.js +7 -1
- package/lib/Socket/stone-call-audio.js +118 -0
- package/lib/Socket/stone-call-keys.js +96 -0
- package/lib/Socket/stone-call-signaling.js +127 -0
- package/lib/Socket/stone-calls.d.ts +76 -0
- package/lib/Socket/stone-calls.js +311 -0
- package/lib/Socket/stone-features.d.ts +13 -0
- package/lib/Socket/stone-features.js +85 -0
- package/lib/Socket/stone-pairing-state.js +7 -0
- package/lib/Socket/stone-voip-bridge.js +263 -0
- package/lib/Socket/stone-voip-codec.js +33 -0
- package/lib/Socket/stone-voip-group.js +112 -0
- package/lib/Socket/stone-voip-identity.js +26 -0
- package/lib/Socket/stone-voip-offer-audit.js +49 -0
- package/lib/Socket/stone-voip-transport.js +44 -0
- package/lib/Types/Events.d.ts +55 -0
- package/lib/Utils/messages-media.js +15 -1
- package/lib/Utils/validate-connection.js +2 -0
- package/package.json +20 -4
- package/lib/Socket/chats.js.bak +0 -1198
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stone WPP - proteção de grupos baseada em sinais observados.
|
|
3
|
+
* Não modifica mensagens, não falsifica provas e não substitui o retry criptográfico.
|
|
4
|
+
*/
|
|
5
|
+
const PAYMENT_FIELDS = new Set([
|
|
6
|
+
'requestPaymentMessage', 'sendPaymentMessage', 'paymentInviteMessage',
|
|
7
|
+
'cancelPaymentRequestMessage', 'declinePaymentRequestMessage',
|
|
8
|
+
'orderMessage', 'invoiceMessage'
|
|
9
|
+
])
|
|
10
|
+
const PAYMENT_BUTTONS = new Set(['payment_info', 'review_and_pay', 'payment_instruction'])
|
|
11
|
+
const DEFAULTS = Object.freeze({
|
|
12
|
+
enabled: true,
|
|
13
|
+
mode: 'alert', // alert | delete | remove | protect
|
|
14
|
+
paymentBurst: 3,
|
|
15
|
+
messageBurst: 14,
|
|
16
|
+
windowMs: 8000,
|
|
17
|
+
lockMs: 12000,
|
|
18
|
+
observeInvisible: true,
|
|
19
|
+
observePayments: true,
|
|
20
|
+
observeQuotes: true,
|
|
21
|
+
observeFlood: true,
|
|
22
|
+
editNotice: false,
|
|
23
|
+
observeReceipts: true,
|
|
24
|
+
receiptBurst: 6,
|
|
25
|
+
immunity: [],
|
|
26
|
+
historyLimit: 100
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const GROUP_SUFFIX = '@g.us'
|
|
30
|
+
const MAX_MONITORS = 3
|
|
31
|
+
const MAX_SEEN = 6000
|
|
32
|
+
const SEEN_TTL = 120000
|
|
33
|
+
|
|
34
|
+
// Compara apenas o mesmo domínio de JID: PN e LID não são intercambiáveis.
|
|
35
|
+
const canonicalJid = jid => {
|
|
36
|
+
if (typeof jid !== 'string' || !jid.includes('@')) return ''
|
|
37
|
+
const [user, domain] = jid.split('@')
|
|
38
|
+
return user.split(':')[0] + '@' + domain.toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
const sameUser = (a, b) => !!canonicalJid(a) && canonicalJid(a) === canonicalJid(b)
|
|
41
|
+
const isImmune = (identity, list) => Array.isArray(list) && list.some(jid => sameUser(jid, identity))
|
|
42
|
+
const STORE_PREFIX = 'stone:groupProtection:'
|
|
43
|
+
const storedSettings = settings => Object.fromEntries(Object.keys(DEFAULTS).map(key => [key, settings[key]]))
|
|
44
|
+
const recordSafe = event => ({
|
|
45
|
+
jid: event.jid, participant: event.participant || null,
|
|
46
|
+
reason: event.reason || null, severity: event.severity || null,
|
|
47
|
+
action: event.action || null, mode: event.mode || event.attemptedMode || null,
|
|
48
|
+
deleted: !!event.deleted, removed: !!event.removed, locked: !!event.locked,
|
|
49
|
+
receivedAt: event.receivedAt || Date.now()
|
|
50
|
+
})
|
|
51
|
+
const isAdmin = p => !!p?.admin
|
|
52
|
+
const boundedInt = (v, min, max, key) => {
|
|
53
|
+
if (!Number.isInteger(v) || v < min || v > max) {
|
|
54
|
+
throw new RangeError(`${key} deve ser um inteiro entre ${min} e ${max}`)
|
|
55
|
+
}
|
|
56
|
+
return v
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scanPayment(root) {
|
|
60
|
+
if (!root || typeof root !== 'object') return null
|
|
61
|
+
const seen = new WeakSet()
|
|
62
|
+
const todo = [{ node: root, depth: 0 }]
|
|
63
|
+
let checked = 0
|
|
64
|
+
while (todo.length && checked++ < 180) {
|
|
65
|
+
const { node, depth } = todo.pop()
|
|
66
|
+
if (!node || typeof node !== 'object' || seen.has(node) || depth > 7) continue
|
|
67
|
+
seen.add(node)
|
|
68
|
+
for (const [key, value] of Object.entries(node)) {
|
|
69
|
+
if (!value || key === 'quotedMessage') continue
|
|
70
|
+
if (PAYMENT_FIELDS.has(key)) return key
|
|
71
|
+
if (key === 'nativeFlowMessage' && Array.isArray(value.buttons)) {
|
|
72
|
+
for (const button of value.buttons) {
|
|
73
|
+
if (PAYMENT_BUTTONS.has(String(button?.name || '').toLowerCase())) return 'nativeFlowPayment'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'object') todo.push({ node: value, depth: depth + 1 })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findQuoted(root) {
|
|
83
|
+
let message = root
|
|
84
|
+
for (let i = 0; i < 5 && message && typeof message === 'object'; i++) {
|
|
85
|
+
for (const key of ['extendedTextMessage', 'imageMessage', 'videoMessage', 'documentMessage', 'buttonsResponseMessage', 'templateButtonReplyMessage']) {
|
|
86
|
+
const context = message[key]?.contextInfo
|
|
87
|
+
if (context?.quotedMessage) return { message: context.quotedMessage, participant: context.participant, stanzaId: context.stanzaId }
|
|
88
|
+
}
|
|
89
|
+
message = message.ephemeralMessage?.message || message.viewOnceMessage?.message || message.viewOnceMessageV2?.message || message.documentWithCaptionMessage?.message
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Instala proteção opcional em um socket sem alterar a recepção nativa. */
|
|
95
|
+
export function createGroupProtectionMethods(socket) {
|
|
96
|
+
const groups = new Map()
|
|
97
|
+
const observers = new Map()
|
|
98
|
+
const seen = new Map()
|
|
99
|
+
const buckets = new Map()
|
|
100
|
+
const busy = new Set()
|
|
101
|
+
const recentActions = new Map()
|
|
102
|
+
const locks = new Map()
|
|
103
|
+
const persistence = new Map()
|
|
104
|
+
const managed = new Map()
|
|
105
|
+
|
|
106
|
+
function markOnce(key) {
|
|
107
|
+
const now = Date.now()
|
|
108
|
+
if (seen.has(key) && now - seen.get(key) < SEEN_TTL) return false
|
|
109
|
+
seen.set(key, now)
|
|
110
|
+
if (seen.size > MAX_SEEN) {
|
|
111
|
+
for (const [k, t] of seen) if (now - t >= SEEN_TTL) seen.delete(k)
|
|
112
|
+
while (seen.size > MAX_SEEN) seen.delete(seen.keys().next().value)
|
|
113
|
+
}
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function count(group, participant, kind, windowMs, mark) {
|
|
118
|
+
const key = `${group}\u0000${participant}\u0000${kind}`
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
const old = (buckets.get(key) || []).filter(item => now - item.at < windowMs)
|
|
121
|
+
if (mark && !old.some(item => item.id === mark)) old.push({ id: mark, at: now })
|
|
122
|
+
buckets.set(key, old.slice(-80))
|
|
123
|
+
if (buckets.size > 3000) {
|
|
124
|
+
for (const [k, entries] of buckets) {
|
|
125
|
+
if (!entries.length || now - entries[entries.length - 1].at > 2 * windowMs) buckets.delete(k)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return old.length
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function appendHistory(state, type, incident) {
|
|
132
|
+
const entry = { type, ...recordSafe(incident) }
|
|
133
|
+
state.history.push(entry)
|
|
134
|
+
if (state.history.length > state.settings.historyLimit) state.history.splice(0, state.history.length - state.settings.historyLimit)
|
|
135
|
+
if (state.store) {
|
|
136
|
+
const key = STORE_PREFIX + 'history:' + entry.jid
|
|
137
|
+
const previous = persistence.get(key) || Promise.resolve()
|
|
138
|
+
const task = previous.catch(() => {}).then(async () => {
|
|
139
|
+
await state.store.set(key, state.history.slice())
|
|
140
|
+
})
|
|
141
|
+
persistence.set(key, task)
|
|
142
|
+
task.catch(error => socket.logger?.warn?.({ error }, 'groupProtection history save failed'))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function emit(name, data) {
|
|
146
|
+
const state = groups.get(data?.jid)
|
|
147
|
+
if (state && (name === 'group-protection.alert' || name === 'group-protection.action')) {
|
|
148
|
+
appendHistory(state, name.endsWith('.alert') ? 'alert' : 'action', data)
|
|
149
|
+
}
|
|
150
|
+
try { socket.ev.emit(name, data) } catch (error) {
|
|
151
|
+
socket.logger?.warn?.({ error }, 'groupProtection event handler failed')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function act(incident, message) {
|
|
156
|
+
const state = groups.get(incident.jid)
|
|
157
|
+
if (!state || state.settings.mode === 'alert' || incident.severity !== 'high' || !incident.verified) return
|
|
158
|
+
const lockKey = `${incident.jid}\u0000${incident.participant}`
|
|
159
|
+
if (busy.has(lockKey) || Date.now() - (recentActions.get(lockKey) || 0) < 30000) return
|
|
160
|
+
busy.add(lockKey)
|
|
161
|
+
recentActions.set(lockKey, Date.now())
|
|
162
|
+
if (recentActions.size > 3000) {
|
|
163
|
+
for (const [key, at] of recentActions) if (Date.now() - at > 60000) recentActions.delete(key)
|
|
164
|
+
}
|
|
165
|
+
const result = { ...incident, attemptedMode: state.settings.mode, deleted: false, removed: false, locked: false }
|
|
166
|
+
let notice = null
|
|
167
|
+
try {
|
|
168
|
+
const metadata = await socket.groupMetadata(incident.jid)
|
|
169
|
+
const members = Array.isArray(metadata?.participants) ? metadata.participants : []
|
|
170
|
+
const self = members.find(p => sameUser(p.id, socket.user?.id) || sameUser(p.id, socket.authState?.creds?.me?.lid))
|
|
171
|
+
const actor = members.find(p => sameUser(p.id, incident.participant))
|
|
172
|
+
if (!isAdmin(self)) {
|
|
173
|
+
result.error = 'bot_not_admin'
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
if (!actor || isAdmin(actor) || sameUser(actor.id, self.id)) {
|
|
177
|
+
result.error = 'target_unverified_or_admin'
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
if (isImmune(actor.id, state.settings.immunity)
|
|
181
|
+
|| isImmune(actor.phoneNumber, state.settings.immunity)
|
|
182
|
+
|| isImmune(actor.lid, state.settings.immunity)
|
|
183
|
+
|| isImmune(incident.participant, state.settings.immunity)) {
|
|
184
|
+
result.error = 'immune_participant'
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
if (state.settings.editNotice) {
|
|
188
|
+
try {
|
|
189
|
+
notice = await socket.sendMessage(incident.jid, { text: 'Stone Protection: ameaça identificada. Verificando e aplicando as ações de moderação...' })
|
|
190
|
+
} catch (error) { result.noticeError = String(error?.message || error) }
|
|
191
|
+
}
|
|
192
|
+
if (state.settings.mode === 'protect' && !metadata.announce && !locks.has(incident.jid)) {
|
|
193
|
+
try {
|
|
194
|
+
await socket.groupSettingUpdate(incident.jid, 'announcement')
|
|
195
|
+
result.locked = true
|
|
196
|
+
const timer = setTimeout(async () => {
|
|
197
|
+
locks.delete(incident.jid)
|
|
198
|
+
try {
|
|
199
|
+
const latest = await socket.groupMetadata(incident.jid)
|
|
200
|
+
if (latest?.announce === true) await socket.groupSettingUpdate(incident.jid, 'not_announcement')
|
|
201
|
+
}
|
|
202
|
+
catch (error) { emit('group-protection.action', { jid: incident.jid, action: 'reopen', ok: false, error: String(error?.message || error) }) }
|
|
203
|
+
}, state.settings.lockMs)
|
|
204
|
+
timer.unref?.()
|
|
205
|
+
locks.set(incident.jid, timer)
|
|
206
|
+
} catch (error) { result.lockError = String(error?.message || error) }
|
|
207
|
+
}
|
|
208
|
+
if (message?.key?.id && message.key.remoteJid === incident.jid) {
|
|
209
|
+
try {
|
|
210
|
+
await socket.sendMessage(incident.jid, { delete: message.key })
|
|
211
|
+
result.deleted = true
|
|
212
|
+
} catch (error) { result.deleteError = String(error?.message || error) }
|
|
213
|
+
}
|
|
214
|
+
if (state.settings.mode === 'remove' || state.settings.mode === 'protect') {
|
|
215
|
+
try {
|
|
216
|
+
const response = await socket.groupParticipantsUpdate(incident.jid, [actor.id], 'remove')
|
|
217
|
+
result.removed = Array.isArray(response) && response.some(r => String(r.status) === '200')
|
|
218
|
+
} catch (error) { result.removeError = String(error?.message || error) }
|
|
219
|
+
}
|
|
220
|
+
} catch (error) { result.error = String(error?.message || error) }
|
|
221
|
+
finally {
|
|
222
|
+
if (notice?.key?.id && notice.key.fromMe === true) {
|
|
223
|
+
const status = result.removed ? 'participante removido' : result.deleted ? 'mensagem removida' : 'ações parciais ou indisponíveis'
|
|
224
|
+
try {
|
|
225
|
+
await socket.sendMessage(incident.jid, {
|
|
226
|
+
text: `Stone Protection: verificação concluída — ${status}.`,
|
|
227
|
+
edit: notice.key
|
|
228
|
+
})
|
|
229
|
+
result.noticeEdited = true
|
|
230
|
+
} catch (error) {
|
|
231
|
+
result.noticeEditError = String(error?.message || error)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
busy.delete(lockKey)
|
|
235
|
+
state.actions++
|
|
236
|
+
emit('group-protection.action', result)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function report(incident, message) {
|
|
241
|
+
const state = groups.get(incident.jid)
|
|
242
|
+
if (!state) return
|
|
243
|
+
const evidenceKey = `${incident.jid}\u0000${incident.messageId}\u0000${incident.reason}\u0000${incident.participant}`
|
|
244
|
+
if (!markOnce(evidenceKey)) return
|
|
245
|
+
state.alerts++
|
|
246
|
+
emit('group-protection.alert', { ...incident, mode: state.settings.mode, receivedAt: Date.now() })
|
|
247
|
+
void act(incident, message).catch(error => socket.logger?.warn?.({ error }, 'groupProtection action failed'))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function handleSignal(data, source) {
|
|
251
|
+
if (!data?.remoteJid?.endsWith(GROUP_SUFFIX) || !data.messageId) return
|
|
252
|
+
const state = groups.get(data.remoteJid)
|
|
253
|
+
if (!state?.settings.observeInvisible) return
|
|
254
|
+
const actor = data.participant || data.participantAlt
|
|
255
|
+
if (!actor) return
|
|
256
|
+
const signature = String(data.decryptFail || '').toLowerCase()
|
|
257
|
+
const highSignal = signature === 'hide'
|
|
258
|
+
if (!markOnce(`signal\u0000${data.remoteJid}\u0000${data.messageId}\u0000${actor}`)) return
|
|
259
|
+
const hits = count(data.remoteJid, actor, 'ciphertext', state.settings.windowMs, data.messageId)
|
|
260
|
+
// Ciphertext não é prova de ataque; nunca punir só por falha de descriptografia.
|
|
261
|
+
if (highSignal || hits >= state.settings.paymentBurst) {
|
|
262
|
+
report({ jid: data.remoteJid, participant: actor, messageId: data.messageId, reason: 'invisible-suspected', severity: highSignal ? 'medium' : 'low', verified: false, source, evidence: { decryptFail: signature || null, ciphertextCount: hits } })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Recibos são somente telemetria: pedidos de retransmissão são normais
|
|
267
|
+
// em redes instáveis e NÃO identificam culpabilidade de um participante.
|
|
268
|
+
function handleReceipt(data, source) {
|
|
269
|
+
if (!data?.remoteJid?.endsWith(GROUP_SUFFIX) || !data.messageId || !data.participant) return
|
|
270
|
+
const state = groups.get(data.remoteJid)
|
|
271
|
+
if (!state?.settings.observeReceipts || data.type !== 'retry') return
|
|
272
|
+
const mark = `receipt\u0000${data.remoteJid}\u0000${data.messageId}\u0000${data.participant}`
|
|
273
|
+
if (!markOnce(mark)) return
|
|
274
|
+
const n = count(data.remoteJid, data.participant, 'retry-receipt', state.settings.windowMs, data.messageId)
|
|
275
|
+
if (n === state.settings.receiptBurst || (n > state.settings.receiptBurst && n % state.settings.receiptBurst === 0)) {
|
|
276
|
+
report({ jid: data.remoteJid, participant: data.participant,
|
|
277
|
+
messageId: data.messageId, reason: 'receipt-suspected', severity: 'low',
|
|
278
|
+
verified: false, source, evidence: { type: data.type, receiptCount: n } })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function handlePdo(data, source) {
|
|
283
|
+
if (!data?.remoteJid?.endsWith(GROUP_SUFFIX)) return
|
|
284
|
+
const state = groups.get(data.remoteJid)
|
|
285
|
+
if (!state?.settings.observeInvisible || !data.messageId) return
|
|
286
|
+
emit('group-protection.pdo-observed', {
|
|
287
|
+
jid: data.remoteJid, messageId: data.messageId,
|
|
288
|
+
status: data.status, source, receivedAt: Date.now()
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleMessage(message, source, kind = 'notify') {
|
|
293
|
+
const jid = message?.key?.remoteJid
|
|
294
|
+
if (!jid?.endsWith(GROUP_SUFFIX) || message.key.fromMe) return
|
|
295
|
+
const state = groups.get(jid)
|
|
296
|
+
if (!state || (kind !== 'notify' && kind !== 'append')) return
|
|
297
|
+
const id = message.key.id
|
|
298
|
+
const participant = message.key.participant || message.participant || message.key.participantAlt
|
|
299
|
+
if (!id || !participant || !message.message) return
|
|
300
|
+
const stamp = Number(message.messageTimestamp || 0)
|
|
301
|
+
if (stamp && Math.abs(Date.now() - stamp * 1000) > 120000) return
|
|
302
|
+
if (!markOnce(`message\u0000${jid}\u0000${id}`)) return
|
|
303
|
+
const settings = state.settings
|
|
304
|
+
const paymentType = settings.observePayments ? scanPayment(message.message) : null
|
|
305
|
+
const volume = settings.observeFlood ? count(jid, participant, 'messages', settings.windowMs, id) : 0
|
|
306
|
+
if (paymentType) {
|
|
307
|
+
const payments = count(jid, participant, 'payment', settings.windowMs, id)
|
|
308
|
+
report({ jid, participant, messageId: id, reason: 'payment', severity: payments >= settings.paymentBurst ? 'high' : 'low', verified: payments >= settings.paymentBurst, source, evidence: { paymentType, paymentCount: payments } }, message)
|
|
309
|
+
}
|
|
310
|
+
if (settings.observeFlood && volume >= settings.messageBurst) {
|
|
311
|
+
report({ jid, participant, messageId: id, reason: 'flood', severity: 'high', verified: true, source, evidence: { messageCount: volume } }, message)
|
|
312
|
+
}
|
|
313
|
+
// Citações são evidência auxiliar, nunca prova para punir outro participante.
|
|
314
|
+
if (settings.observeQuotes) {
|
|
315
|
+
const quoted = findQuoted(message.message)
|
|
316
|
+
const quotePayment = quoted && scanPayment(quoted.message)
|
|
317
|
+
if (quotePayment) {
|
|
318
|
+
report({ jid, participant: quoted.participant || participant, messageId: quoted.stanzaId || id, reason: 'quoted-payment', severity: 'medium', verified: false, source, evidence: { quotedBy: participant, paymentType: quotePayment } })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function connectObserver(monitor, index) {
|
|
324
|
+
if (!monitor?.ev || typeof monitor.ev.on !== 'function') throw new TypeError('monitor deve ser um socket Stone WPP conectado')
|
|
325
|
+
if (observers.has(monitor)) return
|
|
326
|
+
const source = index === -1 ? 'primary' : `monitor-${index + 1}`
|
|
327
|
+
const allowed = groupJid => monitor === socket || groups.get(groupJid)?.monitors.includes(monitor)
|
|
328
|
+
const onUpsert = payload => {
|
|
329
|
+
for (const message of payload?.messages || []) {
|
|
330
|
+
if (allowed(message?.key?.remoteJid)) handleMessage(message, source, payload?.type)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const onSignal = payload => {
|
|
334
|
+
if (allowed(payload?.remoteJid)) handleSignal(payload, source)
|
|
335
|
+
}
|
|
336
|
+
monitor.ev.on('messages.upsert', onUpsert)
|
|
337
|
+
const onReceipt = payload => {
|
|
338
|
+
if (allowed(payload?.remoteJid)) handleReceipt(payload, source)
|
|
339
|
+
}
|
|
340
|
+
const onPdo = payload => {
|
|
341
|
+
if (allowed(payload?.remoteJid)) handlePdo(payload, source)
|
|
342
|
+
}
|
|
343
|
+
monitor.ev.on('group-protection.ciphertext', onSignal)
|
|
344
|
+
monitor.ev.on('group-protection.receipt', onReceipt)
|
|
345
|
+
monitor.ev.on('group-protection.pdo', onPdo)
|
|
346
|
+
observers.set(monitor, () => {
|
|
347
|
+
monitor.ev.off('messages.upsert', onUpsert)
|
|
348
|
+
monitor.ev.off('group-protection.ciphertext', onSignal)
|
|
349
|
+
monitor.ev.off('group-protection.receipt', onReceipt)
|
|
350
|
+
monitor.ev.off('group-protection.pdo', onPdo)
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pruneObservers() {
|
|
355
|
+
const active = new Set([...groups.values()].flatMap(g => g.monitors))
|
|
356
|
+
for (const [monitor, detach] of observers) {
|
|
357
|
+
if (monitor !== socket && !active.has(monitor)) { detach(); observers.delete(monitor) }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function statusFor(jid, state, extra = {}) {
|
|
361
|
+
if (!state) return { jid, enabled: false, monitorCount: 0, alerts: 0, actions: 0, ...extra }
|
|
362
|
+
return { jid, ...state.settings, monitorCount: state.monitors.length,
|
|
363
|
+
alerts: state.alerts, actions: state.actions, ...extra }
|
|
364
|
+
}
|
|
365
|
+
function stopManaged(jid) {
|
|
366
|
+
for (const [key, active] of managed) {
|
|
367
|
+
if (!key.startsWith(jid + '\u0000')) continue
|
|
368
|
+
active.cancelled = true
|
|
369
|
+
if (active.timer) clearTimeout(active.timer)
|
|
370
|
+
if (active.socket && active.onUpdate) active.socket.ev.off('connection.update', active.onUpdate)
|
|
371
|
+
managed.delete(key)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function startManaged(jid, index, factory, state, reconnectMs) {
|
|
375
|
+
const key = jid + '\u0000' + index
|
|
376
|
+
if (managed.get(key)?.factory === factory) return
|
|
377
|
+
const current = managed.get(key)
|
|
378
|
+
if (current) {
|
|
379
|
+
current.cancelled = true
|
|
380
|
+
if (current.timer) clearTimeout(current.timer)
|
|
381
|
+
if (current.socket && current.onUpdate) current.socket.ev.off('connection.update', current.onUpdate)
|
|
382
|
+
}
|
|
383
|
+
const item = { factory, timer: null, socket: null, onUpdate: null, attempt: 0, cancelled: false }
|
|
384
|
+
managed.set(key, item)
|
|
385
|
+
const attach = async () => {
|
|
386
|
+
if (item.cancelled || groups.get(jid) !== state) return
|
|
387
|
+
item.attempt++
|
|
388
|
+
try {
|
|
389
|
+
const create = typeof factory === 'function' ? factory : factory.create
|
|
390
|
+
const monitor = await create({ jid, attempt: item.attempt })
|
|
391
|
+
if (!monitor?.ev || typeof monitor.ev.on !== 'function' || monitor === socket) {
|
|
392
|
+
throw new TypeError('monitorFactory deve retornar outro socket autenticado')
|
|
393
|
+
}
|
|
394
|
+
if (item.cancelled || groups.get(jid) !== state) return
|
|
395
|
+
// Pareamento autorizado: o proprietário recebe o código via callback.
|
|
396
|
+
// Nenhuma sessão é vinculada sem a confirmação no próprio WhatsApp.
|
|
397
|
+
if (typeof factory === 'object' && factory.phoneNumber
|
|
398
|
+
&& monitor.authState?.creds?.registered === false) {
|
|
399
|
+
if (typeof monitor.requestPairingCode !== 'function') {
|
|
400
|
+
throw new TypeError('socket auxiliar não suporta requestPairingCode')
|
|
401
|
+
}
|
|
402
|
+
const code = await monitor.requestPairingCode(factory.phoneNumber)
|
|
403
|
+
await factory.onPairingCode({ jid, index, code })
|
|
404
|
+
}
|
|
405
|
+
if (item.socket && item.onUpdate) item.socket.ev.off('connection.update', item.onUpdate)
|
|
406
|
+
const previous = item.socket
|
|
407
|
+
item.socket = monitor
|
|
408
|
+
if (previous) {
|
|
409
|
+
state.monitors = state.monitors.filter(m => m !== previous)
|
|
410
|
+
}
|
|
411
|
+
if (!state.monitors.includes(monitor)) state.monitors.push(monitor)
|
|
412
|
+
connectObserver(monitor, index)
|
|
413
|
+
pruneObservers()
|
|
414
|
+
item.onUpdate = update => {
|
|
415
|
+
if (update?.connection === 'open') {
|
|
416
|
+
item.attempt = 0
|
|
417
|
+
emit('group-protection.monitor', { jid, index, status: 'connected' })
|
|
418
|
+
}
|
|
419
|
+
if (update?.connection !== 'close' || item.cancelled) return
|
|
420
|
+
const code = update?.lastDisconnect?.error?.output?.statusCode
|
|
421
|
+
// Logged out: é necessário parear novamente com consentimento.
|
|
422
|
+
if (code === 401) {
|
|
423
|
+
monitor.ev.off('connection.update', item.onUpdate)
|
|
424
|
+
state.monitors = state.monitors.filter(m => m !== monitor)
|
|
425
|
+
item.socket = null
|
|
426
|
+
pruneObservers()
|
|
427
|
+
emit('group-protection.monitor', { jid, index, status: 'logged-out' })
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
monitor.ev.off('connection.update', item.onUpdate)
|
|
431
|
+
state.monitors = state.monitors.filter(m => m !== monitor)
|
|
432
|
+
pruneObservers()
|
|
433
|
+
schedule()
|
|
434
|
+
}
|
|
435
|
+
monitor.ev.on('connection.update', item.onUpdate)
|
|
436
|
+
emit('group-protection.monitor', { jid, index, status: 'attached' })
|
|
437
|
+
} catch (error) {
|
|
438
|
+
emit('group-protection.monitor', { jid, index, status: 'error',
|
|
439
|
+
error: String(error?.message || error) })
|
|
440
|
+
schedule()
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const schedule = () => {
|
|
444
|
+
if (item.cancelled || groups.get(jid) !== state) return
|
|
445
|
+
const ms = Math.min(60000, reconnectMs * Math.pow(2, Math.min(item.attempt - 1, 5)))
|
|
446
|
+
item.timer = setTimeout(() => { item.timer = null; void attach() }, ms)
|
|
447
|
+
item.timer.unref?.()
|
|
448
|
+
}
|
|
449
|
+
await attach()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
connectObserver(socket, -1)
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
/**
|
|
456
|
+
* Consulta / configura a proteção. Monitors: sockets Stone extras autenticados
|
|
457
|
+
* separadamente e presentes nos mesmos grupos (até três). A Stone não cria sessões
|
|
458
|
+
* extras automaticamente nem promete acessar mensagens omitidas pelo WhatsApp.
|
|
459
|
+
*/
|
|
460
|
+
async groupProtection(jid, options) {
|
|
461
|
+
if (typeof jid !== 'string' || !jid.endsWith(GROUP_SUFFIX)) {
|
|
462
|
+
throw new TypeError('groupProtection requer JID de grupo (@g.us)')
|
|
463
|
+
}
|
|
464
|
+
const old = groups.get(jid)
|
|
465
|
+
if (options === undefined) return statusFor(jid, old)
|
|
466
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
467
|
+
throw new TypeError('options deve ser um objeto')
|
|
468
|
+
}
|
|
469
|
+
const store = options.store || old?.store
|
|
470
|
+
if (store && (typeof store.get !== 'function' || typeof store.set !== 'function')) {
|
|
471
|
+
throw new TypeError('store requer métodos get e set')
|
|
472
|
+
}
|
|
473
|
+
if (options.getHistory === true) {
|
|
474
|
+
const limit = options.limit === undefined ? 100 : boundedInt(options.limit, 1, 500, 'limit')
|
|
475
|
+
const saved = store ? await store.get(STORE_PREFIX + 'history:' + jid) : null
|
|
476
|
+
const history = old?.history?.length ? old.history : (Array.isArray(saved) ? saved : [])
|
|
477
|
+
return statusFor(jid, old, { history: history.slice(-limit) })
|
|
478
|
+
}
|
|
479
|
+
if (options.enabled === false) {
|
|
480
|
+
const savedStore = store
|
|
481
|
+
groups.delete(jid)
|
|
482
|
+
stopManaged(jid)
|
|
483
|
+
pruneObservers()
|
|
484
|
+
if (savedStore) {
|
|
485
|
+
await savedStore.set(STORE_PREFIX + 'settings:' + jid, { enabled: false })
|
|
486
|
+
}
|
|
487
|
+
return statusFor(jid, null)
|
|
488
|
+
}
|
|
489
|
+
let restored = null
|
|
490
|
+
if (options.restore === true) {
|
|
491
|
+
if (!store) throw new TypeError('restore requer store')
|
|
492
|
+
restored = await store.get(STORE_PREFIX + 'settings:' + jid)
|
|
493
|
+
if (!restored?.enabled) return statusFor(jid, old, { restored: false })
|
|
494
|
+
}
|
|
495
|
+
const input = { ...(old?.settings || DEFAULTS), ...(restored || {}), ...options, enabled: true }
|
|
496
|
+
if (!['alert', 'delete', 'remove', 'protect'].includes(input.mode)) {
|
|
497
|
+
throw new TypeError('mode: alert, delete, remove ou protect')
|
|
498
|
+
}
|
|
499
|
+
for (const key of ['observeInvisible', 'observePayments', 'observeQuotes', 'observeFlood', 'editNotice', 'observeReceipts']) {
|
|
500
|
+
if (typeof input[key] !== 'boolean') throw new TypeError(`${key} deve ser booleano`)
|
|
501
|
+
}
|
|
502
|
+
input.paymentBurst = boundedInt(input.paymentBurst, 2, 30, 'paymentBurst')
|
|
503
|
+
input.messageBurst = boundedInt(input.messageBurst, 3, 100, 'messageBurst')
|
|
504
|
+
input.receiptBurst = boundedInt(input.receiptBurst, 3, 50, 'receiptBurst')
|
|
505
|
+
input.windowMs = boundedInt(input.windowMs, 1000, 60000, 'windowMs')
|
|
506
|
+
input.lockMs = boundedInt(input.lockMs, 3000, 120000, 'lockMs')
|
|
507
|
+
input.historyLimit = boundedInt(input.historyLimit, 20, 500, 'historyLimit')
|
|
508
|
+
if (!Array.isArray(input.immunity) || input.immunity.length > 200
|
|
509
|
+
|| !input.immunity.every(jid => typeof jid === 'string' && canonicalJid(jid))) {
|
|
510
|
+
throw new TypeError('immunity deve conter até 200 JIDs válidos')
|
|
511
|
+
}
|
|
512
|
+
input.immunity = [...new Set(input.immunity.map(canonicalJid))]
|
|
513
|
+
const staticMonitors = options.monitors === undefined ? (old?.staticMonitors || []) : options.monitors
|
|
514
|
+
const factories = options.monitorFactories === undefined ? (old?.factories || []) : options.monitorFactories
|
|
515
|
+
if (!Array.isArray(staticMonitors) || !Array.isArray(factories)
|
|
516
|
+
|| staticMonitors.length + factories.length > MAX_MONITORS
|
|
517
|
+
|| new Set(staticMonitors).size !== staticMonitors.length || staticMonitors.includes(socket)
|
|
518
|
+
|| !factories.every(f => typeof f === 'function' ||
|
|
519
|
+
(f && typeof f === 'object' && typeof f.create === 'function'
|
|
520
|
+
&& (!f.phoneNumber || (typeof f.phoneNumber === 'string'
|
|
521
|
+
&& /^\d{8,15}$/.test(f.phoneNumber)
|
|
522
|
+
&& typeof f.onPairingCode === 'function'))))) {
|
|
523
|
+
throw new TypeError('Até 3 monitores: sockets distintos e/ou monitorFactories')
|
|
524
|
+
}
|
|
525
|
+
for (const monitor of staticMonitors) {
|
|
526
|
+
if (!monitor?.ev || typeof monitor.ev.on !== 'function') {
|
|
527
|
+
throw new TypeError('monitor deve ser socket Stone válido')
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const reconnectMs = options.monitorReconnectMs === undefined
|
|
531
|
+
? (old?.monitorReconnectMs || 3000)
|
|
532
|
+
: boundedInt(options.monitorReconnectMs, 1000, 60000, 'monitorReconnectMs')
|
|
533
|
+
const settings = storedSettings(input)
|
|
534
|
+
const state = old || { alerts: 0, actions: 0, history: [], monitors: [] }
|
|
535
|
+
state.settings = settings
|
|
536
|
+
state.staticMonitors = staticMonitors
|
|
537
|
+
state.factories = factories
|
|
538
|
+
state.monitorReconnectMs = reconnectMs
|
|
539
|
+
state.store = store
|
|
540
|
+
state.monitors = [...staticMonitors, ...[...managed.entries()]
|
|
541
|
+
.filter(([key, item]) => key.startsWith(jid + '\u0000')
|
|
542
|
+
&& !item.cancelled && item.socket && factories.includes(item.factory))
|
|
543
|
+
.map(([, item]) => item.socket)]
|
|
544
|
+
if (store && !old?.history?.length) {
|
|
545
|
+
const history = await store.get(STORE_PREFIX + 'history:' + jid)
|
|
546
|
+
if (Array.isArray(history)) state.history = history.slice(-settings.historyLimit)
|
|
547
|
+
}
|
|
548
|
+
groups.set(jid, state)
|
|
549
|
+
staticMonitors.forEach((monitor, index) => connectObserver(monitor, index))
|
|
550
|
+
for (const [key, item] of managed) {
|
|
551
|
+
if (!key.startsWith(jid + '\u0000')) continue
|
|
552
|
+
const index = Number(key.slice((jid + '\u0000').length))
|
|
553
|
+
if (factories[index] !== item.factory) {
|
|
554
|
+
item.cancelled = true
|
|
555
|
+
if (item.timer) clearTimeout(item.timer)
|
|
556
|
+
if (item.socket && item.onUpdate) item.socket.ev.off('connection.update', item.onUpdate)
|
|
557
|
+
state.monitors = state.monitors.filter(monitor => monitor !== item.socket)
|
|
558
|
+
managed.delete(key)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (let i = 0; i < factories.length; i++) {
|
|
562
|
+
await startManaged(jid, i, factories[i], state, reconnectMs)
|
|
563
|
+
}
|
|
564
|
+
pruneObservers()
|
|
565
|
+
if (store) {
|
|
566
|
+
await store.set(STORE_PREFIX + 'settings:' + jid, settings)
|
|
567
|
+
}
|
|
568
|
+
return statusFor(jid, state, { ...(options.restore ? { restored: true } : {}) })
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
package/lib/Socket/index.d.ts
CHANGED
|
@@ -256,5 +256,5 @@ declare const makeWASocket: (config: UserFacingSocketConfig) => {
|
|
|
256
256
|
}[] | undefined>;
|
|
257
257
|
fetchAccountReachoutTimelock: () => Promise<import("../index.js").ReachoutTimelockState>;
|
|
258
258
|
fetchNewChatMessageCap: () => Promise<import("../index.js").NewChatMessageCapInfo>;
|
|
259
|
-
} & import('./stone-features.js').StoneAdditionalMethods;
|
|
259
|
+
} & import('./stone-features.js').StoneAdditionalMethods & import('./group-protection.js').GroupProtectionMethods & { calls: import('./stone-calls.js').StoneCalls };
|
|
260
260
|
export default makeWASocket;
|
package/lib/Socket/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
2
2
|
import { makeCommunitiesSocket } from './communities.js';
|
|
3
3
|
import { createStoneAdditionalMethods } from './stone-features.js';
|
|
4
|
+
import { createGroupProtectionMethods } from './group-protection.js';
|
|
5
|
+
import { createStoneCalls } from './stone-calls.js';
|
|
4
6
|
// export the last socket layer
|
|
5
7
|
const makeWASocket = (config) => {
|
|
6
8
|
const newConfig = {
|
|
@@ -8,6 +10,6 @@ const makeWASocket = (config) => {
|
|
|
8
10
|
...config
|
|
9
11
|
};
|
|
10
12
|
const socket = makeCommunitiesSocket(newConfig);
|
|
11
|
-
return Object.assign(socket, createStoneAdditionalMethods(socket));
|
|
13
|
+
return Object.assign(socket, createStoneAdditionalMethods(socket), createGroupProtectionMethods(socket), { calls: createStoneCalls(socket) });
|
|
12
14
|
};
|
|
13
15
|
export default makeWASocket;
|