@bellaxchuu/flowred 0.8.6
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/LICENSE +21 -0
- package/README.md +214 -0
- package/WAProto/GenerateStatics.sh +3 -0
- package/WAProto/WAProto.proto +6902 -0
- package/WAProto/fix-imports.js +85 -0
- package/WAProto/index.js +242946 -0
- package/engine-requirements.js +10 -0
- package/lib/Defaults/index.js +190 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/Group/ciphertext-message.js +15 -0
- package/lib/Signal/Group/group-session-builder.js +92 -0
- package/lib/Signal/Group/group_cipher.js +89 -0
- package/lib/Signal/Group/index.js +136 -0
- package/lib/Signal/Group/keyhelper.js +73 -0
- package/lib/Signal/Group/sender-chain-key.js +32 -0
- package/lib/Signal/Group/sender-key-distribution-message.js +66 -0
- package/lib/Signal/Group/sender-key-message.js +69 -0
- package/lib/Signal/Group/sender-key-name.js +50 -0
- package/lib/Signal/Group/sender-key-record.js +44 -0
- package/lib/Signal/Group/sender-key-state.js +97 -0
- package/lib/Signal/Group/sender-message-key.js +30 -0
- package/lib/Signal/libsignal.js +490 -0
- package/lib/Signal/lid-mapping.js +262 -0
- package/lib/Socket/Client/index.js +30 -0
- package/lib/Socket/Client/types.js +13 -0
- package/lib/Socket/Client/websocket.js +62 -0
- package/lib/Socket/aigroups.js +240 -0
- package/lib/Socket/business.js +422 -0
- package/lib/Socket/chats.js +1816 -0
- package/lib/Socket/communities.js +549 -0
- package/lib/Socket/graphql.js +656 -0
- package/lib/Socket/groups.js +764 -0
- package/lib/Socket/index.js +39 -0
- package/lib/Socket/interactive-handler.js +522 -0
- package/lib/Socket/interop.js +549 -0
- package/lib/Socket/luxu.js +387 -0
- package/lib/Socket/managed-account.js +183 -0
- package/lib/Socket/messages-recv.js +2440 -0
- package/lib/Socket/messages-send.js +1853 -0
- package/lib/Socket/mex.js +60 -0
- package/lib/Socket/newsletter.js +820 -0
- package/lib/Socket/privacy.js +449 -0
- package/lib/Socket/registration.js +427 -0
- package/lib/Socket/socket.js +1092 -0
- package/lib/Socket/text-router.js +83 -0
- package/lib/Socket/username.js +243 -0
- package/lib/Store/index.js +36 -0
- package/lib/Store/keyed-db.js +108 -0
- package/lib/Store/make-cache-manager-store.js +90 -0
- package/lib/Store/make-in-memory-store.js +604 -0
- package/lib/Store/make-ordered-dictionary.js +81 -0
- package/lib/Store/object-repository.js +29 -0
- package/lib/Types/Auth.js +38 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +4 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +27 -0
- package/lib/Types/LabelAssociation.js +9 -0
- package/lib/Types/Message.js +95 -0
- package/lib/Types/Mex.js +112 -0
- package/lib/Types/Newsletter.js +121 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +2 -0
- package/lib/Types/State.js +70 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +55 -0
- package/lib/Utils/auth-utils.js +306 -0
- package/lib/Utils/browser-utils.js +114 -0
- package/lib/Utils/business.js +247 -0
- package/lib/Utils/chat-utils.js +1208 -0
- package/lib/Utils/command-loader.js +100 -0
- package/lib/Utils/companion-reg-client-utils.js +42 -0
- package/lib/Utils/consumer-application.js +107 -0
- package/lib/Utils/crypto.js +155 -0
- package/lib/Utils/curve25519-js.js +275 -0
- package/lib/Utils/decode-wa-message.js +560 -0
- package/lib/Utils/event-buffer.js +607 -0
- package/lib/Utils/generics.js +563 -0
- package/lib/Utils/group-history.js +60 -0
- package/lib/Utils/history.js +244 -0
- package/lib/Utils/identity-change-handler.js +52 -0
- package/lib/Utils/index.js +57 -0
- package/lib/Utils/jid-display-normalization.js +218 -0
- package/lib/Utils/link-preview.js +143 -0
- package/lib/Utils/logger.js +9 -0
- package/lib/Utils/lt-hash.js +39 -0
- package/lib/Utils/make-mutex.js +36 -0
- package/lib/Utils/message-composer.js +479 -0
- package/lib/Utils/message-retry-manager.js +270 -0
- package/lib/Utils/messages-media.js +896 -0
- package/lib/Utils/messages.js +2904 -0
- package/lib/Utils/meta-ai-msmsg.js +262 -0
- package/lib/Utils/native-bridge.js +82 -0
- package/lib/Utils/noise-handler.js +196 -0
- package/lib/Utils/offline-node-processor.js +42 -0
- package/lib/Utils/pre-key-manager.js +107 -0
- package/lib/Utils/process-message.js +1048 -0
- package/lib/Utils/reporting-utils.js +262 -0
- package/lib/Utils/session-pool.js +94 -0
- package/lib/Utils/signal.js +224 -0
- package/lib/Utils/stanza-ack.js +40 -0
- package/lib/Utils/sticker.js +145 -0
- package/lib/Utils/sync-action-utils.js +54 -0
- package/lib/Utils/tc-token-utils.js +161 -0
- package/lib/Utils/use-multi-file-auth-state.js +121 -0
- package/lib/Utils/validate-connection.js +219 -0
- package/lib/Utils/view-once-cache.js +79 -0
- package/lib/Utils/voip-rekey.js +22 -0
- package/lib/WABinary/constants.js +1304 -0
- package/lib/WABinary/decode.js +343 -0
- package/lib/WABinary/encode.js +228 -0
- package/lib/WABinary/generic-utils.js +240 -0
- package/lib/WABinary/index.js +33 -0
- package/lib/WABinary/jid-utils.js +361 -0
- package/lib/WABinary/types.js +2 -0
- package/lib/WAM/BinaryInfo.js +13 -0
- package/lib/WAM/constants.js +39486 -0
- package/lib/WAM/encode.js +142 -0
- package/lib/WAM/index.js +31 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +55 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +54 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +59 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +31 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +29 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +39 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
- package/lib/WAUSync/Protocols/index.js +39 -0
- package/lib/WAUSync/USyncQuery.js +133 -0
- package/lib/WAUSync/USyncUser.js +50 -0
- package/lib/WAUSync/index.js +31 -0
- package/lib/antiban.js +4637 -0
- package/lib/index.js +45 -0
- package/package.json +107 -0
package/lib/antiban.js
ADDED
|
@@ -0,0 +1,4637 @@
|
|
|
1
|
+
const __importMetaUrl = require('url').pathToFileURL(__filename).href
|
|
2
|
+
;('use strict')
|
|
3
|
+
var __create = Object.create
|
|
4
|
+
var __defProp = Object.defineProperty
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true })
|
|
11
|
+
}
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if ((from && typeof from === 'object') || typeof from === 'function') {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, {
|
|
17
|
+
get: () => from[key],
|
|
18
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
return to
|
|
22
|
+
}
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (
|
|
24
|
+
(target = mod != null ? __create(__getProtoOf(mod)) : {}),
|
|
25
|
+
__copyProps(
|
|
26
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, 'default', { value: mod, enumerable: true }) : target,
|
|
31
|
+
mod
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
var __toCommonJS = mod => __copyProps(__defProp({}, '__esModule', { value: true }), mod)
|
|
35
|
+
|
|
36
|
+
// index.js
|
|
37
|
+
var index_exports = {}
|
|
38
|
+
__export(index_exports, {
|
|
39
|
+
AntiBan: () => AntiBan,
|
|
40
|
+
ContactGraphWarmer: () => ContactGraphWarmer,
|
|
41
|
+
ContentVariator: () => ContentVariator,
|
|
42
|
+
FileStateAdapter: () => FileStateAdapter,
|
|
43
|
+
HealthMonitor: () => HealthMonitor,
|
|
44
|
+
JidCanonicalizer: () => JidCanonicalizer,
|
|
45
|
+
LidFirstResolver: () => LidFirstResolver,
|
|
46
|
+
LidResolver: () => LidResolver,
|
|
47
|
+
MAC_ERROR_CODES: () => MAC_ERROR_CODES,
|
|
48
|
+
MessageQueue: () => MessageQueue,
|
|
49
|
+
MessageRetryReason: () => MessageRetryReason,
|
|
50
|
+
PRESETS: () => PRESETS,
|
|
51
|
+
PostReconnectThrottle: () => PostReconnectThrottle,
|
|
52
|
+
PresenceChoreographer: () => PresenceChoreographer,
|
|
53
|
+
RateLimiter: () => RateLimiter,
|
|
54
|
+
ReplyRatioGuard: () => ReplyRatioGuard,
|
|
55
|
+
RetryReasonTracker: () => RetryReasonTracker,
|
|
56
|
+
Scheduler: () => Scheduler,
|
|
57
|
+
SessionHealthMonitor: () => SessionHealthMonitor,
|
|
58
|
+
StateManager: () => StateManager,
|
|
59
|
+
TimelockGuard: () => TimelockGuard,
|
|
60
|
+
WarmUp: () => WarmUp,
|
|
61
|
+
WebhookAlerts: () => WebhookAlerts,
|
|
62
|
+
applyFingerprint: () => applyFingerprint,
|
|
63
|
+
applyGroupMultiplier: () => applyGroupMultiplier,
|
|
64
|
+
classifyDisconnect: () => classifyDisconnect,
|
|
65
|
+
createLidFirstResolver: () => createLidFirstResolver,
|
|
66
|
+
credsSnapshot: () => credsSnapshot,
|
|
67
|
+
generateFingerprint: () => generateFingerprint,
|
|
68
|
+
getCircadianMultiplier: () => getCircadianMultiplier,
|
|
69
|
+
getRetryReasonDescription: () => getRetryReasonDescription,
|
|
70
|
+
isBroadcast: () => isBroadcast,
|
|
71
|
+
isGroup: () => isGroup,
|
|
72
|
+
isMacError: () => isMacError,
|
|
73
|
+
isNewsletter: () => isNewsletter,
|
|
74
|
+
messageRecovery: () => messageRecovery,
|
|
75
|
+
parseRetryReason: () => parseRetryReason,
|
|
76
|
+
proxyRotator: () => proxyRotator,
|
|
77
|
+
readReceiptVariance: () => readReceiptVariance,
|
|
78
|
+
resolveConfig: () => resolveConfig,
|
|
79
|
+
shouldUseGroupProfile: () => shouldUseGroupProfile,
|
|
80
|
+
wrapSocket: () => wrapSocket,
|
|
81
|
+
wrapWithSessionStability: () => wrapWithSessionStability
|
|
82
|
+
})
|
|
83
|
+
module.exports = __toCommonJS(index_exports)
|
|
84
|
+
|
|
85
|
+
// rateLimiter.js
|
|
86
|
+
var TIME_CONSTANTS = {
|
|
87
|
+
MS_PER_SECOND: 1e3,
|
|
88
|
+
MS_PER_MINUTE: 6e4,
|
|
89
|
+
MS_PER_HOUR: 36e5,
|
|
90
|
+
MS_PER_DAY: 864e5,
|
|
91
|
+
BURST_RESET_MS: 3e4,
|
|
92
|
+
IDENTICAL_WINDOW_MS: 36e5
|
|
93
|
+
// 1 hour
|
|
94
|
+
}
|
|
95
|
+
var DEFAULT_CONFIG = {
|
|
96
|
+
maxPerMinute: 8,
|
|
97
|
+
maxPerHour: 200,
|
|
98
|
+
maxPerDay: 1500,
|
|
99
|
+
minDelayMs: 1500,
|
|
100
|
+
maxDelayMs: 5e3,
|
|
101
|
+
newChatDelayMs: 3e3,
|
|
102
|
+
maxIdenticalMessages: 3,
|
|
103
|
+
burstAllowance: 3,
|
|
104
|
+
identicalMessageWindowMs: TIME_CONSTANTS.IDENTICAL_WINDOW_MS
|
|
105
|
+
}
|
|
106
|
+
var RateLimiter = class {
|
|
107
|
+
config
|
|
108
|
+
messages = []
|
|
109
|
+
identicalCount = /* @__PURE__ */ new Map()
|
|
110
|
+
knownChats = /* @__PURE__ */ new Set()
|
|
111
|
+
burstCount = 0
|
|
112
|
+
lastMessageTime = 0
|
|
113
|
+
constructor(config = {}) {
|
|
114
|
+
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Calculate delay before next message can be sent.
|
|
118
|
+
* Returns 0 if message can be sent immediately.
|
|
119
|
+
* Returns -1 if message should be blocked entirely.
|
|
120
|
+
*/
|
|
121
|
+
async getDelay(recipient, content) {
|
|
122
|
+
const now = Date.now()
|
|
123
|
+
this.cleanup(now)
|
|
124
|
+
const contentHash = this.hashContent(content)
|
|
125
|
+
const dayMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY)
|
|
126
|
+
if (dayMessages.length >= this.config.maxPerDay) {
|
|
127
|
+
return -1
|
|
128
|
+
}
|
|
129
|
+
const hourMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR)
|
|
130
|
+
if (hourMessages.length >= this.config.maxPerHour) {
|
|
131
|
+
hourMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
132
|
+
const oldestInHour = hourMessages[0]
|
|
133
|
+
const delay2 = oldestInHour
|
|
134
|
+
? oldestInHour.timestamp + TIME_CONSTANTS.MS_PER_HOUR - now
|
|
135
|
+
: TIME_CONSTANTS.MS_PER_HOUR
|
|
136
|
+
return Math.max(delay2, TIME_CONSTANTS.MS_PER_MINUTE)
|
|
137
|
+
}
|
|
138
|
+
const minuteMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE)
|
|
139
|
+
if (minuteMessages.length >= this.config.maxPerMinute) {
|
|
140
|
+
minuteMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
141
|
+
const oldestInMinute = minuteMessages[0]
|
|
142
|
+
const delay2 = oldestInMinute
|
|
143
|
+
? oldestInMinute.timestamp + TIME_CONSTANTS.MS_PER_MINUTE - now
|
|
144
|
+
: TIME_CONSTANTS.MS_PER_MINUTE
|
|
145
|
+
return Math.max(delay2, TIME_CONSTANTS.MS_PER_SECOND)
|
|
146
|
+
}
|
|
147
|
+
const tracker = this.identicalCount.get(contentHash)
|
|
148
|
+
if (tracker) {
|
|
149
|
+
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
150
|
+
if (tracker.count >= this.config.maxIdenticalMessages) {
|
|
151
|
+
return -1
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
let delay = 0
|
|
156
|
+
if (this.burstCount < this.config.burstAllowance) {
|
|
157
|
+
this.burstCount++
|
|
158
|
+
delay = this.jitter(this.config.minDelayMs * 0.5, this.config.minDelayMs)
|
|
159
|
+
} else {
|
|
160
|
+
delay = this.jitter(this.config.minDelayMs, this.config.maxDelayMs)
|
|
161
|
+
}
|
|
162
|
+
const isInterop = recipient.endsWith('@interop')
|
|
163
|
+
if (!this.knownChats.has(recipient) || isInterop) {
|
|
164
|
+
delay += this.jitter(this.config.newChatDelayMs * 0.5, this.config.newChatDelayMs)
|
|
165
|
+
}
|
|
166
|
+
const timeSinceLast = now - this.lastMessageTime
|
|
167
|
+
if (timeSinceLast < this.config.minDelayMs) {
|
|
168
|
+
delay = Math.max(delay, this.config.minDelayMs - timeSinceLast)
|
|
169
|
+
}
|
|
170
|
+
const typingDelay = Math.min(content.length * 30, 3e3)
|
|
171
|
+
delay += this.jitter(typingDelay * 0.5, typingDelay)
|
|
172
|
+
return Math.round(delay)
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Record a sent message
|
|
176
|
+
*/
|
|
177
|
+
record(recipient, content) {
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
const contentHash = this.hashContent(content)
|
|
180
|
+
const timeSinceLast = now - this.lastMessageTime
|
|
181
|
+
if (timeSinceLast > TIME_CONSTANTS.BURST_RESET_MS) {
|
|
182
|
+
this.burstCount = 0
|
|
183
|
+
}
|
|
184
|
+
this.messages.push({ timestamp: now, recipient, contentHash })
|
|
185
|
+
this.knownChats.add(recipient)
|
|
186
|
+
this.lastMessageTime = now
|
|
187
|
+
const tracker = this.identicalCount.get(contentHash)
|
|
188
|
+
if (tracker) {
|
|
189
|
+
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
190
|
+
tracker.count++
|
|
191
|
+
tracker.lastSeen = now
|
|
192
|
+
} else {
|
|
193
|
+
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now })
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get current usage stats
|
|
201
|
+
*/
|
|
202
|
+
getStats() {
|
|
203
|
+
const now = Date.now()
|
|
204
|
+
this.cleanup(now)
|
|
205
|
+
return {
|
|
206
|
+
lastMinute: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE).length,
|
|
207
|
+
lastHour: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR).length,
|
|
208
|
+
lastDay: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY).length,
|
|
209
|
+
limits: {
|
|
210
|
+
perMinute: this.config.maxPerMinute,
|
|
211
|
+
perHour: this.config.maxPerHour,
|
|
212
|
+
perDay: this.config.maxPerDay
|
|
213
|
+
},
|
|
214
|
+
knownChats: this.knownChats.size
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Get the set of known chat JIDs (for state persistence) */
|
|
218
|
+
getKnownChats() {
|
|
219
|
+
return this.knownChats
|
|
220
|
+
}
|
|
221
|
+
/** Restore known chats from persisted state */
|
|
222
|
+
restoreKnownChats(chats) {
|
|
223
|
+
for (const jid of chats) {
|
|
224
|
+
this.knownChats.add(jid)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
cleanup(now) {
|
|
228
|
+
this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY)
|
|
229
|
+
for (const [hash, tracker] of this.identicalCount.entries()) {
|
|
230
|
+
if (now - tracker.lastSeen > this.config.identicalMessageWindowMs) {
|
|
231
|
+
this.identicalCount.delete(hash)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Random delay between min and max (gaussian-ish distribution) */
|
|
236
|
+
jitter(min, max) {
|
|
237
|
+
const u1 = Math.random()
|
|
238
|
+
const u2 = Math.random()
|
|
239
|
+
const normal = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
|
|
240
|
+
const normalized = (normal + 3) / 6
|
|
241
|
+
const clamped = Math.max(0, Math.min(1, normalized))
|
|
242
|
+
return Math.round(min + clamped * (max - min))
|
|
243
|
+
}
|
|
244
|
+
/** Simple hash for content dedup */
|
|
245
|
+
hashContent(content) {
|
|
246
|
+
let hash = 0
|
|
247
|
+
for (let i = 0; i < content.length; i++) {
|
|
248
|
+
const char = content.charCodeAt(i)
|
|
249
|
+
hash = (hash << 5) - hash + char
|
|
250
|
+
hash |= 0
|
|
251
|
+
}
|
|
252
|
+
return hash.toString(36)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// warmup.js
|
|
257
|
+
var DEFAULT_CONFIG2 = {
|
|
258
|
+
warmUpDays: 7,
|
|
259
|
+
day1Limit: 20,
|
|
260
|
+
growthFactor: 1.8,
|
|
261
|
+
inactivityThresholdHours: 72
|
|
262
|
+
}
|
|
263
|
+
var WarmUp = class {
|
|
264
|
+
config
|
|
265
|
+
state
|
|
266
|
+
constructor(config = {}, existingState) {
|
|
267
|
+
this.config = { ...DEFAULT_CONFIG2, ...config }
|
|
268
|
+
this.state = existingState || this.freshState()
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the current daily message limit based on warm-up phase
|
|
272
|
+
*/
|
|
273
|
+
getDailyLimit() {
|
|
274
|
+
if (this.state.graduated) return Infinity
|
|
275
|
+
const day = this.getCurrentDay()
|
|
276
|
+
if (day >= this.config.warmUpDays) {
|
|
277
|
+
this.state.graduated = true
|
|
278
|
+
return Infinity
|
|
279
|
+
}
|
|
280
|
+
return Math.round(this.config.day1Limit * Math.pow(this.config.growthFactor, day))
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if a message can be sent (within warm-up limits)
|
|
284
|
+
*/
|
|
285
|
+
canSend() {
|
|
286
|
+
this.checkInactivity()
|
|
287
|
+
if (this.state.graduated) return true
|
|
288
|
+
const day = this.getCurrentDay()
|
|
289
|
+
const todayCount = this.state.dailyCounts[day] || 0
|
|
290
|
+
return todayCount < this.getDailyLimit()
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Record a sent message
|
|
294
|
+
*/
|
|
295
|
+
record() {
|
|
296
|
+
const now = Date.now()
|
|
297
|
+
const day = this.getCurrentDay()
|
|
298
|
+
while (this.state.dailyCounts.length <= day) {
|
|
299
|
+
this.state.dailyCounts.push(0)
|
|
300
|
+
}
|
|
301
|
+
this.state.dailyCounts[day]++
|
|
302
|
+
this.state.lastActiveAt = now
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get current warm-up status
|
|
306
|
+
*/
|
|
307
|
+
getStatus() {
|
|
308
|
+
const day = this.getCurrentDay()
|
|
309
|
+
const todaySent = this.state.dailyCounts[day] || 0
|
|
310
|
+
const limit = this.getDailyLimit()
|
|
311
|
+
return {
|
|
312
|
+
phase: this.state.graduated ? 'graduated' : 'warming',
|
|
313
|
+
day: Math.min(day + 1, this.config.warmUpDays),
|
|
314
|
+
totalDays: this.config.warmUpDays,
|
|
315
|
+
todayLimit: limit === Infinity ? -1 : limit,
|
|
316
|
+
todaySent,
|
|
317
|
+
progress: this.state.graduated ? 100 : Math.round((day / this.config.warmUpDays) * 100)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Export state for persistence
|
|
322
|
+
*/
|
|
323
|
+
exportState() {
|
|
324
|
+
return { ...this.state }
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Reset warm-up (e.g., after detected ban risk)
|
|
328
|
+
*/
|
|
329
|
+
reset() {
|
|
330
|
+
this.state = this.freshState()
|
|
331
|
+
}
|
|
332
|
+
getCurrentDay() {
|
|
333
|
+
return Math.floor((Date.now() - this.state.startedAt) / 864e5)
|
|
334
|
+
}
|
|
335
|
+
checkInactivity() {
|
|
336
|
+
const hoursSinceActive = (Date.now() - this.state.lastActiveAt) / 36e5
|
|
337
|
+
if (hoursSinceActive > this.config.inactivityThresholdHours && this.state.graduated) {
|
|
338
|
+
this.state = this.freshState()
|
|
339
|
+
this.state.graduated = false
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
freshState() {
|
|
343
|
+
const now = Date.now()
|
|
344
|
+
return {
|
|
345
|
+
startedAt: now,
|
|
346
|
+
lastActiveAt: now,
|
|
347
|
+
dailyCounts: [],
|
|
348
|
+
graduated: false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// health.js
|
|
354
|
+
var DEFAULT_CONFIG3 = {
|
|
355
|
+
disconnectWarningThreshold: 3,
|
|
356
|
+
disconnectCriticalThreshold: 5,
|
|
357
|
+
failedMessageThreshold: 5,
|
|
358
|
+
autoPauseAt: 'high'
|
|
359
|
+
}
|
|
360
|
+
var HealthMonitor = class {
|
|
361
|
+
config
|
|
362
|
+
events = []
|
|
363
|
+
startTime = Date.now()
|
|
364
|
+
paused = false
|
|
365
|
+
lastRisk = 'low'
|
|
366
|
+
lastBadEventTime = Date.now()
|
|
367
|
+
lastEventWasSevere = false
|
|
368
|
+
constructor(config = {}) {
|
|
369
|
+
this.config = { ...DEFAULT_CONFIG3, ...config }
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Record a disconnection event
|
|
373
|
+
*/
|
|
374
|
+
recordDisconnect(reason) {
|
|
375
|
+
const reasonStr = String(reason)
|
|
376
|
+
if (reasonStr === '403' || reasonStr === 'forbidden') {
|
|
377
|
+
this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr })
|
|
378
|
+
this.lastBadEventTime = Date.now()
|
|
379
|
+
this.lastEventWasSevere = true
|
|
380
|
+
} else if (reasonStr === '401' || reasonStr === 'loggedOut') {
|
|
381
|
+
this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr })
|
|
382
|
+
this.lastBadEventTime = Date.now()
|
|
383
|
+
this.lastEventWasSevere = true
|
|
384
|
+
} else if (reasonStr === '463') {
|
|
385
|
+
this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail: reasonStr })
|
|
386
|
+
this.lastBadEventTime = Date.now()
|
|
387
|
+
this.lastEventWasSevere = false
|
|
388
|
+
} else {
|
|
389
|
+
this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr })
|
|
390
|
+
this.lastBadEventTime = Date.now()
|
|
391
|
+
this.lastEventWasSevere = false
|
|
392
|
+
}
|
|
393
|
+
this.checkAndNotify()
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Record a successful reconnection
|
|
397
|
+
*/
|
|
398
|
+
recordReconnect() {
|
|
399
|
+
this.events.push({ type: 'reconnect', timestamp: Date.now() })
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Record a failed message send
|
|
403
|
+
*/
|
|
404
|
+
recordMessageFailed(error) {
|
|
405
|
+
this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error })
|
|
406
|
+
this.lastBadEventTime = Date.now()
|
|
407
|
+
this.lastEventWasSevere = false
|
|
408
|
+
this.checkAndNotify()
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Record a 463 reachout timelock error
|
|
412
|
+
*/
|
|
413
|
+
recordReachoutTimelock(detail) {
|
|
414
|
+
this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail })
|
|
415
|
+
this.lastBadEventTime = Date.now()
|
|
416
|
+
this.lastEventWasSevere = false
|
|
417
|
+
this.checkAndNotify()
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get current health status
|
|
421
|
+
*/
|
|
422
|
+
getStatus() {
|
|
423
|
+
const now = Date.now()
|
|
424
|
+
this.cleanup(now)
|
|
425
|
+
const hourEvents = this.events.filter(e => now - e.timestamp < 36e5)
|
|
426
|
+
const disconnects = hourEvents.filter(e => e.type === 'disconnect').length
|
|
427
|
+
const forbidden = hourEvents.filter(e => e.type === 'forbidden').length
|
|
428
|
+
const loggedOut = hourEvents.filter(e => e.type === 'loggedOut').length
|
|
429
|
+
const failedMessages = hourEvents.filter(e => e.type === 'messageFailed').length
|
|
430
|
+
let score = 0
|
|
431
|
+
const reasons = []
|
|
432
|
+
if (forbidden > 0) {
|
|
433
|
+
score += 40 * forbidden
|
|
434
|
+
reasons.push(`${forbidden} forbidden (403) error${forbidden > 1 ? 's' : ''} in last hour`)
|
|
435
|
+
}
|
|
436
|
+
if (loggedOut > 0) {
|
|
437
|
+
score += 60
|
|
438
|
+
reasons.push('Logged out by WhatsApp \u2014 possible temporary ban')
|
|
439
|
+
}
|
|
440
|
+
const timelocked = hourEvents.filter(e => e.type === 'reachoutTimelocked').length
|
|
441
|
+
if (timelocked > 0) {
|
|
442
|
+
score += 25
|
|
443
|
+
reasons.push(`${timelocked} reachout timelock (463) error${timelocked > 1 ? 's' : ''} in last hour`)
|
|
444
|
+
}
|
|
445
|
+
if (disconnects >= this.config.disconnectCriticalThreshold) {
|
|
446
|
+
score += 30
|
|
447
|
+
reasons.push(`${disconnects} disconnects in last hour (critical threshold)`)
|
|
448
|
+
} else if (disconnects >= this.config.disconnectWarningThreshold) {
|
|
449
|
+
score += 30
|
|
450
|
+
reasons.push(`${disconnects} disconnects in last hour`)
|
|
451
|
+
}
|
|
452
|
+
if (failedMessages >= this.config.failedMessageThreshold) {
|
|
453
|
+
score += 20
|
|
454
|
+
reasons.push(`${failedMessages} failed messages in last hour`)
|
|
455
|
+
}
|
|
456
|
+
score = Math.min(100, score)
|
|
457
|
+
const minutesSinceLastBad = (now - this.lastBadEventTime) / 6e4
|
|
458
|
+
const decayRate = this.lastEventWasSevere ? 2 : 5
|
|
459
|
+
score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate))
|
|
460
|
+
let risk
|
|
461
|
+
if (score >= 80) risk = 'critical'
|
|
462
|
+
else if (score >= 40) risk = 'high'
|
|
463
|
+
else if (score >= 15) risk = 'medium'
|
|
464
|
+
else risk = 'low'
|
|
465
|
+
let recommendation
|
|
466
|
+
switch (risk) {
|
|
467
|
+
case 'critical':
|
|
468
|
+
recommendation = 'STOP ALL MESSAGING IMMEDIATELY. Disconnect and wait 24-48 hours before reconnecting.'
|
|
469
|
+
break
|
|
470
|
+
case 'high':
|
|
471
|
+
recommendation = 'Reduce messaging rate by 80%. Consider pausing for 1-2 hours.'
|
|
472
|
+
break
|
|
473
|
+
case 'medium':
|
|
474
|
+
recommendation = 'Reduce messaging rate by 50%. Increase delays between messages.'
|
|
475
|
+
break
|
|
476
|
+
default:
|
|
477
|
+
recommendation = 'Operating normally. Continue monitoring.'
|
|
478
|
+
}
|
|
479
|
+
const lastDisconnect = [...this.events]
|
|
480
|
+
.reverse()
|
|
481
|
+
.find(e => e.type === 'disconnect' || e.type === 'forbidden' || e.type === 'loggedOut')
|
|
482
|
+
return {
|
|
483
|
+
risk,
|
|
484
|
+
score,
|
|
485
|
+
reasons: reasons.length ? reasons : ['No issues detected'],
|
|
486
|
+
recommendation,
|
|
487
|
+
stats: {
|
|
488
|
+
disconnectsLastHour: disconnects,
|
|
489
|
+
failedMessagesLastHour: failedMessages,
|
|
490
|
+
forbiddenErrors: forbidden,
|
|
491
|
+
timelockErrors: timelocked,
|
|
492
|
+
uptimeMs: now - this.startTime,
|
|
493
|
+
lastDisconnectReason: lastDisconnect?.detail
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Check if sending should be paused
|
|
499
|
+
*/
|
|
500
|
+
isPaused() {
|
|
501
|
+
if (this.paused) return true
|
|
502
|
+
const status = this.getStatus()
|
|
503
|
+
const riskOrder = ['low', 'medium', 'high', 'critical']
|
|
504
|
+
return riskOrder.indexOf(status.risk) >= riskOrder.indexOf(this.config.autoPauseAt)
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Manually pause/resume
|
|
508
|
+
*/
|
|
509
|
+
setPaused(paused) {
|
|
510
|
+
this.paused = paused
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Reset all tracked events
|
|
514
|
+
*/
|
|
515
|
+
reset() {
|
|
516
|
+
this.events = []
|
|
517
|
+
this.startTime = Date.now()
|
|
518
|
+
this.paused = false
|
|
519
|
+
this.lastRisk = 'low'
|
|
520
|
+
this.lastBadEventTime = Date.now()
|
|
521
|
+
this.lastEventWasSevere = false
|
|
522
|
+
}
|
|
523
|
+
cleanup(now) {
|
|
524
|
+
this.events = this.events.filter(e => now - e.timestamp < 216e5)
|
|
525
|
+
}
|
|
526
|
+
checkAndNotify() {
|
|
527
|
+
const status = this.getStatus()
|
|
528
|
+
if (status.risk !== this.lastRisk) {
|
|
529
|
+
this.lastRisk = status.risk
|
|
530
|
+
this.config.onRiskChange?.(status)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// timelockGuard.js
|
|
536
|
+
var DEFAULT_CONFIG4 = {
|
|
537
|
+
resumeBufferMs: 1e4
|
|
538
|
+
}
|
|
539
|
+
var TimelockGuard = class {
|
|
540
|
+
config
|
|
541
|
+
state = {
|
|
542
|
+
isActive: false,
|
|
543
|
+
errorCount: 0
|
|
544
|
+
}
|
|
545
|
+
knownChats = /* @__PURE__ */ new Set()
|
|
546
|
+
resumeTimer = null
|
|
547
|
+
timerGeneration = 0
|
|
548
|
+
// BUG FIX 4: Track timer validity to prevent race conditions
|
|
549
|
+
constructor(config = {}) {
|
|
550
|
+
this.config = { ...DEFAULT_CONFIG4, ...config }
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Update timelock state from Baileys connection.update event
|
|
554
|
+
*/
|
|
555
|
+
onTimelockUpdate(data) {
|
|
556
|
+
const wasActive = this.state.isActive
|
|
557
|
+
this.state.isActive = !!data.isActive
|
|
558
|
+
this.state.enforcementType = data.enforcementType
|
|
559
|
+
this.state.expiresAt = data.timeEnforcementEnds
|
|
560
|
+
if (this.state.isActive && !wasActive) {
|
|
561
|
+
this.state.detectedAt = /* @__PURE__ */ new Date()
|
|
562
|
+
this.state.errorCount = 0
|
|
563
|
+
this.config.onTimelockDetected?.(this.getState())
|
|
564
|
+
this.scheduleResume()
|
|
565
|
+
} else if (this.state.isActive && wasActive) {
|
|
566
|
+
this.scheduleResume()
|
|
567
|
+
}
|
|
568
|
+
if (!this.state.isActive && wasActive) {
|
|
569
|
+
this.clearResumeTimer()
|
|
570
|
+
this.config.onTimelockLifted?.(this.getState())
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Record a 463 error from a failed send
|
|
575
|
+
*/
|
|
576
|
+
record463Error() {
|
|
577
|
+
this.state.errorCount++
|
|
578
|
+
if (!this.state.isActive) {
|
|
579
|
+
this.state.isActive = true
|
|
580
|
+
this.state.detectedAt = /* @__PURE__ */ new Date()
|
|
581
|
+
this.state.expiresAt = new Date(Date.now() + 6e4)
|
|
582
|
+
this.config.onTimelockDetected?.(this.getState())
|
|
583
|
+
this.scheduleResume()
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Register a JID as a known/existing chat (has tctoken / prior history)
|
|
588
|
+
*/
|
|
589
|
+
registerKnownChat(jid) {
|
|
590
|
+
this.knownChats.add(jid)
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Register multiple known chats at once (e.g. from chat list on connect)
|
|
594
|
+
*/
|
|
595
|
+
registerKnownChats(jids) {
|
|
596
|
+
for (const jid of jids) {
|
|
597
|
+
this.knownChats.add(jid)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Check if a message to this recipient should be allowed
|
|
602
|
+
*/
|
|
603
|
+
canSend(jid) {
|
|
604
|
+
if (!this.state.isActive) {
|
|
605
|
+
return { allowed: true }
|
|
606
|
+
}
|
|
607
|
+
if (this.state.expiresAt) {
|
|
608
|
+
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs
|
|
609
|
+
if (Date.now() >= expiryWithBuffer) {
|
|
610
|
+
this.lift()
|
|
611
|
+
return { allowed: true }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (jid.endsWith('@g.us') || jid.endsWith('@newsletter')) {
|
|
615
|
+
return { allowed: true }
|
|
616
|
+
}
|
|
617
|
+
if (this.knownChats.has(jid)) {
|
|
618
|
+
return { allowed: true }
|
|
619
|
+
}
|
|
620
|
+
const expiresIn = this.state.expiresAt ? Math.max(0, this.state.expiresAt.getTime() - Date.now()) : 6e4
|
|
621
|
+
return {
|
|
622
|
+
allowed: false,
|
|
623
|
+
reason: `Reachout timelocked (${this.state.enforcementType || 'unknown'}). New contacts blocked. Expires in ${Math.ceil(expiresIn / 1e3)}s.`
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get current timelock state
|
|
628
|
+
*/
|
|
629
|
+
getState() {
|
|
630
|
+
return { ...this.state }
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Check if currently timelocked
|
|
634
|
+
*/
|
|
635
|
+
isTimelocked() {
|
|
636
|
+
if (!this.state.isActive) return false
|
|
637
|
+
if (this.state.expiresAt) {
|
|
638
|
+
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs
|
|
639
|
+
if (Date.now() >= expiryWithBuffer) {
|
|
640
|
+
this.lift()
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return true
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get the set of known chat JIDs
|
|
648
|
+
*/
|
|
649
|
+
getKnownChats() {
|
|
650
|
+
return new Set(this.knownChats)
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Manually lift the timelock
|
|
654
|
+
*/
|
|
655
|
+
lift() {
|
|
656
|
+
if (this.state.isActive) {
|
|
657
|
+
this.state.isActive = false
|
|
658
|
+
this.clearResumeTimer()
|
|
659
|
+
this.config.onTimelockLifted?.(this.getState())
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Reset all state
|
|
664
|
+
*/
|
|
665
|
+
reset() {
|
|
666
|
+
this.state = { isActive: false, errorCount: 0 }
|
|
667
|
+
this.knownChats.clear()
|
|
668
|
+
this.clearResumeTimer()
|
|
669
|
+
}
|
|
670
|
+
scheduleResume() {
|
|
671
|
+
this.clearResumeTimer()
|
|
672
|
+
if (this.state.expiresAt) {
|
|
673
|
+
const delay = this.state.expiresAt.getTime() - Date.now() + this.config.resumeBufferMs
|
|
674
|
+
if (delay > 0) {
|
|
675
|
+
this.timerGeneration++
|
|
676
|
+
const currentGeneration = this.timerGeneration
|
|
677
|
+
this.resumeTimer = setTimeout(() => {
|
|
678
|
+
if (currentGeneration === this.timerGeneration) {
|
|
679
|
+
this.lift()
|
|
680
|
+
}
|
|
681
|
+
}, delay)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
clearResumeTimer() {
|
|
686
|
+
if (this.resumeTimer) {
|
|
687
|
+
clearTimeout(this.resumeTimer)
|
|
688
|
+
this.resumeTimer = null
|
|
689
|
+
this.timerGeneration++
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// replyRatio.js
|
|
695
|
+
var DEFAULT_CONFIG5 = {
|
|
696
|
+
enabled: false,
|
|
697
|
+
minRatio: 0.1,
|
|
698
|
+
minMessagesBeforeEnforce: 5,
|
|
699
|
+
inboundAutoReplyProbability: 0.25,
|
|
700
|
+
autoReplyTemplates: ['\u{1F44D}', '\u{1F44C}', 'ok', 'noted', 'thanks', '\u{1F64F}', 'got it'],
|
|
701
|
+
cooldownHoursOnViolation: 24,
|
|
702
|
+
scope: 'individual'
|
|
703
|
+
}
|
|
704
|
+
var ReplyRatioGuard = class {
|
|
705
|
+
config
|
|
706
|
+
contacts = /* @__PURE__ */ new Map()
|
|
707
|
+
constructor(config = {}) {
|
|
708
|
+
this.config = { ...DEFAULT_CONFIG5, ...config }
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check if message can be sent to this contact based on reply ratio.
|
|
712
|
+
* Call before sending.
|
|
713
|
+
*/
|
|
714
|
+
beforeSend(jid) {
|
|
715
|
+
if (!this.config.enabled) {
|
|
716
|
+
return { allowed: true }
|
|
717
|
+
}
|
|
718
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
719
|
+
return { allowed: true }
|
|
720
|
+
}
|
|
721
|
+
const record = this.contacts.get(jid)
|
|
722
|
+
if (!record) {
|
|
723
|
+
return { allowed: true }
|
|
724
|
+
}
|
|
725
|
+
if (record.cooledUntil && Date.now() < record.cooledUntil) {
|
|
726
|
+
const hoursLeft = Math.ceil((record.cooledUntil - Date.now()) / 36e5)
|
|
727
|
+
return {
|
|
728
|
+
allowed: false,
|
|
729
|
+
reason: `Reply ratio cooldown \u2014 ${record.sent} sent, ${record.received} received. Retry in ${hoursLeft}h`
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (record.sent >= this.config.minMessagesBeforeEnforce) {
|
|
733
|
+
const ratio = record.sent === 0 ? 1 : record.received / record.sent
|
|
734
|
+
if (ratio < this.config.minRatio) {
|
|
735
|
+
record.cooledUntil = Date.now() + this.config.cooldownHoursOnViolation * 36e5
|
|
736
|
+
return {
|
|
737
|
+
allowed: false,
|
|
738
|
+
reason: `Reply ratio too low (${(ratio * 100).toFixed(1)}% < ${(this.config.minRatio * 100).toFixed(1)}%). Cooldown ${this.config.cooldownHoursOnViolation}h`
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return { allowed: true }
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Record an outbound message sent to this contact.
|
|
746
|
+
*/
|
|
747
|
+
recordSent(jid) {
|
|
748
|
+
if (!this.config.enabled) return
|
|
749
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 }
|
|
750
|
+
record.sent++
|
|
751
|
+
this.contacts.set(jid, record)
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Record an inbound message received from this contact.
|
|
755
|
+
*/
|
|
756
|
+
recordReceived(jid) {
|
|
757
|
+
if (!this.config.enabled) return
|
|
758
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 }
|
|
759
|
+
record.received++
|
|
760
|
+
delete record.cooledUntil
|
|
761
|
+
this.contacts.set(jid, record)
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Suggest whether to send an auto-reply to this incoming message.
|
|
765
|
+
* Returns { shouldReply: true, suggestedText: '👍' } if probability check passes.
|
|
766
|
+
* Caller is responsible for actually sending the message.
|
|
767
|
+
*/
|
|
768
|
+
suggestReply(jid, _msgText) {
|
|
769
|
+
if (!this.config.enabled) {
|
|
770
|
+
return { shouldReply: false }
|
|
771
|
+
}
|
|
772
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
773
|
+
return { shouldReply: false }
|
|
774
|
+
}
|
|
775
|
+
if (Math.random() < this.config.inboundAutoReplyProbability) {
|
|
776
|
+
const templates = this.config.autoReplyTemplates
|
|
777
|
+
const suggestedText = templates[Math.floor(Math.random() * templates.length)]
|
|
778
|
+
return { shouldReply: true, suggestedText }
|
|
779
|
+
}
|
|
780
|
+
return { shouldReply: false }
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Get statistics for all contacts and global metrics.
|
|
784
|
+
*/
|
|
785
|
+
getStats() {
|
|
786
|
+
const perContact = Array.from(this.contacts.entries()).map(([jid, record]) => ({
|
|
787
|
+
jid,
|
|
788
|
+
sent: record.sent,
|
|
789
|
+
received: record.received,
|
|
790
|
+
ratio: record.sent === 0 ? 0 : record.received / record.sent,
|
|
791
|
+
cooledUntil: record.cooledUntil
|
|
792
|
+
}))
|
|
793
|
+
const globalSent = perContact.reduce((sum, c) => sum + c.sent, 0)
|
|
794
|
+
const globalReceived = perContact.reduce((sum, c) => sum + c.received, 0)
|
|
795
|
+
const globalRatio = globalSent === 0 ? 0 : globalReceived / globalSent
|
|
796
|
+
const contactsOnCooldown = perContact.filter(c => c.cooledUntil && Date.now() < c.cooledUntil).length
|
|
797
|
+
return {
|
|
798
|
+
perContact,
|
|
799
|
+
globalSent,
|
|
800
|
+
globalReceived,
|
|
801
|
+
globalRatio,
|
|
802
|
+
contactsOnCooldown
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Reset all counters.
|
|
807
|
+
*/
|
|
808
|
+
reset() {
|
|
809
|
+
this.contacts.clear()
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Export state for persistence.
|
|
813
|
+
*/
|
|
814
|
+
exportState() {
|
|
815
|
+
return {
|
|
816
|
+
contacts: Array.from(this.contacts.entries())
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Restore state from persistence.
|
|
821
|
+
*/
|
|
822
|
+
restoreState(state) {
|
|
823
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
824
|
+
this.contacts = new Map(state.contacts)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Check if JID is a group.
|
|
829
|
+
*/
|
|
830
|
+
isGroup(jid) {
|
|
831
|
+
return jid.endsWith('@g.us')
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// contactGraph.js
|
|
836
|
+
var DEFAULT_CONFIG6 = {
|
|
837
|
+
enabled: false,
|
|
838
|
+
requireHandshakeBeforeGroupSend: true,
|
|
839
|
+
handshakeMinDelayMs: 36e5,
|
|
840
|
+
// 1 hour
|
|
841
|
+
groupLurkPeriodMs: 432e5,
|
|
842
|
+
// 12 hours
|
|
843
|
+
maxStrangerMessagesPerDay: 5,
|
|
844
|
+
autoRegisterOnIncoming: true
|
|
845
|
+
}
|
|
846
|
+
var ContactGraphWarmer = class {
|
|
847
|
+
config
|
|
848
|
+
contacts = /* @__PURE__ */ new Map()
|
|
849
|
+
groups = /* @__PURE__ */ new Map()
|
|
850
|
+
strangerMessagesToday = 0
|
|
851
|
+
lastStrangerResetDay = this.getCurrentDay()
|
|
852
|
+
constructor(config = {}) {
|
|
853
|
+
this.config = { ...DEFAULT_CONFIG6, ...config }
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Check if message can be sent to this contact/group.
|
|
857
|
+
* Returns { allowed: false, needsHandshake: true } if handshake required.
|
|
858
|
+
*/
|
|
859
|
+
canMessage(jid) {
|
|
860
|
+
if (!this.config.enabled) {
|
|
861
|
+
return { allowed: true }
|
|
862
|
+
}
|
|
863
|
+
const currentDay = this.getCurrentDay()
|
|
864
|
+
if (currentDay !== this.lastStrangerResetDay) {
|
|
865
|
+
this.strangerMessagesToday = 0
|
|
866
|
+
this.lastStrangerResetDay = currentDay
|
|
867
|
+
}
|
|
868
|
+
if (this.isGroup(jid)) {
|
|
869
|
+
return this.checkGroupMessage(jid)
|
|
870
|
+
}
|
|
871
|
+
return this.checkIndividualMessage(jid)
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Mark handshake as sent to this contact.
|
|
875
|
+
*/
|
|
876
|
+
markHandshakeSent(jid) {
|
|
877
|
+
if (!this.config.enabled) return
|
|
878
|
+
if (this.isGroup(jid)) return
|
|
879
|
+
const record = this.contacts.get(jid) || { state: 'stranger' }
|
|
880
|
+
record.state = 'handshake_sent'
|
|
881
|
+
record.handshakeSentAt = Date.now()
|
|
882
|
+
this.contacts.set(jid, record)
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Mark handshake as complete with this contact.
|
|
886
|
+
*/
|
|
887
|
+
markHandshakeComplete(jid) {
|
|
888
|
+
if (!this.config.enabled) return
|
|
889
|
+
if (this.isGroup(jid)) return
|
|
890
|
+
const record = this.contacts.get(jid) || { state: 'stranger' }
|
|
891
|
+
record.state = 'handshake_complete'
|
|
892
|
+
this.contacts.set(jid, record)
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Register a contact as known (skip handshake requirement).
|
|
896
|
+
*/
|
|
897
|
+
registerKnownContact(jid) {
|
|
898
|
+
if (!this.config.enabled) return
|
|
899
|
+
if (this.isGroup(jid)) return
|
|
900
|
+
const record = this.contacts.get(jid) || { state: 'stranger' }
|
|
901
|
+
record.state = 'known'
|
|
902
|
+
this.contacts.set(jid, record)
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Register a group join event.
|
|
906
|
+
*/
|
|
907
|
+
registerGroupJoin(groupJid) {
|
|
908
|
+
if (!this.config.enabled) return
|
|
909
|
+
if (!this.isGroup(groupJid)) return
|
|
910
|
+
this.groups.set(groupJid, { joinedAt: Date.now() })
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get contact state.
|
|
914
|
+
*/
|
|
915
|
+
getContactState(jid) {
|
|
916
|
+
if (this.isGroup(jid)) return 'known'
|
|
917
|
+
return this.contacts.get(jid)?.state || 'stranger'
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Handle incoming message — auto-register if enabled.
|
|
921
|
+
*/
|
|
922
|
+
onIncomingMessage(jid) {
|
|
923
|
+
if (!this.config.enabled) return
|
|
924
|
+
if (this.isGroup(jid)) return
|
|
925
|
+
if (this.config.autoRegisterOnIncoming) {
|
|
926
|
+
this.registerKnownContact(jid)
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get statistics.
|
|
931
|
+
*/
|
|
932
|
+
getStats() {
|
|
933
|
+
const knownContacts = Array.from(this.contacts.values()).filter(c => c.state === 'known').length
|
|
934
|
+
const pendingHandshakes = Array.from(this.contacts.values()).filter(c => c.state === 'handshake_sent').length
|
|
935
|
+
const groupsJoined = Array.from(this.groups.entries()).map(([groupJid, record]) => ({
|
|
936
|
+
groupJid,
|
|
937
|
+
joinedAt: record.joinedAt,
|
|
938
|
+
firstSendUnlocksAt: record.joinedAt + this.config.groupLurkPeriodMs
|
|
939
|
+
}))
|
|
940
|
+
return {
|
|
941
|
+
knownContacts,
|
|
942
|
+
pendingHandshakes,
|
|
943
|
+
strangersToday: this.strangerMessagesToday,
|
|
944
|
+
groupsJoined
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Reset all state.
|
|
949
|
+
*/
|
|
950
|
+
reset() {
|
|
951
|
+
this.contacts.clear()
|
|
952
|
+
this.groups.clear()
|
|
953
|
+
this.strangerMessagesToday = 0
|
|
954
|
+
this.lastStrangerResetDay = this.getCurrentDay()
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Export state for persistence.
|
|
958
|
+
*/
|
|
959
|
+
exportState() {
|
|
960
|
+
return {
|
|
961
|
+
contacts: Array.from(this.contacts.entries()),
|
|
962
|
+
groups: Array.from(this.groups.entries()),
|
|
963
|
+
strangerMessagesToday: this.strangerMessagesToday,
|
|
964
|
+
lastStrangerResetDay: this.lastStrangerResetDay
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Restore state from persistence.
|
|
969
|
+
*/
|
|
970
|
+
restoreState(state) {
|
|
971
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
972
|
+
this.contacts = new Map(state.contacts)
|
|
973
|
+
}
|
|
974
|
+
if (state?.groups && Array.isArray(state.groups)) {
|
|
975
|
+
this.groups = new Map(state.groups)
|
|
976
|
+
}
|
|
977
|
+
if (typeof state?.strangerMessagesToday === 'number') {
|
|
978
|
+
this.strangerMessagesToday = state.strangerMessagesToday
|
|
979
|
+
}
|
|
980
|
+
if (typeof state?.lastStrangerResetDay === 'number') {
|
|
981
|
+
this.lastStrangerResetDay = state.lastStrangerResetDay
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Private helpers
|
|
985
|
+
isGroup(jid) {
|
|
986
|
+
return jid.endsWith('@g.us')
|
|
987
|
+
}
|
|
988
|
+
getCurrentDay() {
|
|
989
|
+
return Math.floor(Date.now() / 864e5)
|
|
990
|
+
}
|
|
991
|
+
checkGroupMessage(groupJid) {
|
|
992
|
+
const record = this.groups.get(groupJid)
|
|
993
|
+
if (!record) {
|
|
994
|
+
return { allowed: true }
|
|
995
|
+
}
|
|
996
|
+
const lurkEndsAt = record.joinedAt + this.config.groupLurkPeriodMs
|
|
997
|
+
if (Date.now() < lurkEndsAt) {
|
|
998
|
+
const minutesLeft = Math.ceil((lurkEndsAt - Date.now()) / 6e4)
|
|
999
|
+
return {
|
|
1000
|
+
allowed: false,
|
|
1001
|
+
reason: `Group lurk period not elapsed \u2014 wait ${minutesLeft} minutes`
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { allowed: true }
|
|
1005
|
+
}
|
|
1006
|
+
checkIndividualMessage(jid) {
|
|
1007
|
+
const record = this.contacts.get(jid)
|
|
1008
|
+
if (!record || record.state === 'stranger') {
|
|
1009
|
+
if (this.config.requireHandshakeBeforeGroupSend) {
|
|
1010
|
+
if (this.strangerMessagesToday >= this.config.maxStrangerMessagesPerDay) {
|
|
1011
|
+
return {
|
|
1012
|
+
allowed: false,
|
|
1013
|
+
reason: `Daily new-contact limit reached (${this.config.maxStrangerMessagesPerDay})`,
|
|
1014
|
+
needsHandshake: true
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
this.strangerMessagesToday++
|
|
1018
|
+
}
|
|
1019
|
+
return { allowed: true, needsHandshake: true }
|
|
1020
|
+
}
|
|
1021
|
+
if (record.state === 'handshake_sent') {
|
|
1022
|
+
if (!record.handshakeSentAt) {
|
|
1023
|
+
return { allowed: true }
|
|
1024
|
+
}
|
|
1025
|
+
const elapsed = Date.now() - record.handshakeSentAt
|
|
1026
|
+
if (elapsed < this.config.handshakeMinDelayMs) {
|
|
1027
|
+
const minutesLeft = Math.ceil((this.config.handshakeMinDelayMs - elapsed) / 6e4)
|
|
1028
|
+
return {
|
|
1029
|
+
allowed: false,
|
|
1030
|
+
reason: `Handshake too recent \u2014 wait ${minutesLeft} minutes`
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return { allowed: true }
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// presenceChoreographer.js
|
|
1039
|
+
var DEFAULT_CONFIG7 = {
|
|
1040
|
+
enabled: false,
|
|
1041
|
+
enableCircadianRhythm: true,
|
|
1042
|
+
timezone: 'UTC',
|
|
1043
|
+
activityCurve: 'office',
|
|
1044
|
+
circadian: {
|
|
1045
|
+
enabled: true,
|
|
1046
|
+
profile: 'default',
|
|
1047
|
+
timezone: 'UTC'
|
|
1048
|
+
},
|
|
1049
|
+
distractionPauseProbability: 0.05,
|
|
1050
|
+
distractionPauseMinMs: 3e5,
|
|
1051
|
+
distractionPauseMaxMs: 12e5,
|
|
1052
|
+
readReceiptDelayMinMs: 3e3,
|
|
1053
|
+
readReceiptDelayMaxMs: 45e3,
|
|
1054
|
+
readReceiptSkipProbability: 0.15,
|
|
1055
|
+
offlineGapProbability: 0.03,
|
|
1056
|
+
offlineGapMinMs: 3e5,
|
|
1057
|
+
offlineGapMaxMs: 9e5,
|
|
1058
|
+
enableTypingModel: true,
|
|
1059
|
+
typingWPM: 45,
|
|
1060
|
+
typingWPMStdDev: 15,
|
|
1061
|
+
thinkPauseProbability: 0.08,
|
|
1062
|
+
thinkPauseMinMs: 800,
|
|
1063
|
+
thinkPauseMaxMs: 3500,
|
|
1064
|
+
intermittentPausedProbability: 0.4,
|
|
1065
|
+
typingMaxMs: 9e4,
|
|
1066
|
+
typingMinMs: 600
|
|
1067
|
+
}
|
|
1068
|
+
var ACTIVITY_CURVES = {
|
|
1069
|
+
office: [
|
|
1070
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
|
1071
|
+
// 0-7: night quiet
|
|
1072
|
+
0.5, 0.5,
|
|
1073
|
+
// 8-9: morning ramp
|
|
1074
|
+
0.95, 0.95,
|
|
1075
|
+
// 10-11: morning peak
|
|
1076
|
+
0.6,
|
|
1077
|
+
// 12: lunch dip
|
|
1078
|
+
0.9, 0.9, 0.9, 0.9,
|
|
1079
|
+
// 13-16: afternoon
|
|
1080
|
+
0.6, 0.6,
|
|
1081
|
+
// 17-18: wind-down
|
|
1082
|
+
0.4, 0.4,
|
|
1083
|
+
// 19-20: evening
|
|
1084
|
+
0.2, 0.2, 0.2, 0.2
|
|
1085
|
+
// 21-24: taper
|
|
1086
|
+
],
|
|
1087
|
+
social: [
|
|
1088
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
|
1089
|
+
// 0-7: night quiet
|
|
1090
|
+
0.3, 0.4,
|
|
1091
|
+
// 8-9: slow start
|
|
1092
|
+
0.7, 0.8,
|
|
1093
|
+
// 10-11: ramp up
|
|
1094
|
+
0.5,
|
|
1095
|
+
// 12: lunch
|
|
1096
|
+
0.7, 0.7,
|
|
1097
|
+
// 13-14: afternoon
|
|
1098
|
+
0.4,
|
|
1099
|
+
// 15: tea time dip
|
|
1100
|
+
0.8, 0.9, 0.9,
|
|
1101
|
+
// 16-18: active
|
|
1102
|
+
0.6,
|
|
1103
|
+
// 19: dinner dip
|
|
1104
|
+
0.8, 0.85, 0.9, 0.95, 1
|
|
1105
|
+
// 20-24: evening peak
|
|
1106
|
+
],
|
|
1107
|
+
global: [
|
|
1108
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
|
|
1109
|
+
// 0-5: night
|
|
1110
|
+
0.4, 0.4,
|
|
1111
|
+
// 6-7: dawn dip
|
|
1112
|
+
0.6, 0.7, 0.8, 0.8,
|
|
1113
|
+
// 8-11: morning
|
|
1114
|
+
0.6,
|
|
1115
|
+
// 12: lunch
|
|
1116
|
+
0.8, 0.8, 0.8, 0.8,
|
|
1117
|
+
// 13-16: afternoon
|
|
1118
|
+
0.7, 0.7,
|
|
1119
|
+
// 17-18: evening
|
|
1120
|
+
0.6, 0.5, 0.5, 0.5, 0.5, 0.5
|
|
1121
|
+
// 19-24: night taper
|
|
1122
|
+
]
|
|
1123
|
+
}
|
|
1124
|
+
function getCircadianMultiplier(date = /* @__PURE__ */ new Date(), profile = 'default', timezone) {
|
|
1125
|
+
if (profile === 'always_on') {
|
|
1126
|
+
return 1
|
|
1127
|
+
}
|
|
1128
|
+
let hour
|
|
1129
|
+
if (timezone) {
|
|
1130
|
+
try {
|
|
1131
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
1132
|
+
timeZone: timezone,
|
|
1133
|
+
hour: 'numeric',
|
|
1134
|
+
hour12: false
|
|
1135
|
+
})
|
|
1136
|
+
const parts = formatter.formatToParts(date)
|
|
1137
|
+
const hourPart = parts.find(p => p.type === 'hour')
|
|
1138
|
+
hour = hourPart ? parseInt(hourPart.value, 10) : date.getHours()
|
|
1139
|
+
} catch {
|
|
1140
|
+
hour = date.getHours()
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
hour = date.getHours()
|
|
1144
|
+
}
|
|
1145
|
+
let shiftedHour = hour
|
|
1146
|
+
if (profile === 'nightOwl') {
|
|
1147
|
+
shiftedHour = (hour - 3 + 24) % 24
|
|
1148
|
+
} else if (profile === 'earlyBird') {
|
|
1149
|
+
shiftedHour = (hour + 2) % 24
|
|
1150
|
+
}
|
|
1151
|
+
if (shiftedHour >= 9 && shiftedHour < 22) {
|
|
1152
|
+
const t = (shiftedHour - 9) / 13
|
|
1153
|
+
return 1 + 0.2 * Math.cos(2 * Math.PI * t)
|
|
1154
|
+
} else if (shiftedHour >= 22 && shiftedHour < 24) {
|
|
1155
|
+
const t = (shiftedHour - 22) / 2
|
|
1156
|
+
return 1.2 + 1.3 * t
|
|
1157
|
+
} else if (shiftedHour >= 0 && shiftedHour < 2) {
|
|
1158
|
+
const t = shiftedHour / 2
|
|
1159
|
+
return 2.5 + 1.5 * t
|
|
1160
|
+
} else if (shiftedHour >= 2 && shiftedHour < 6) {
|
|
1161
|
+
const t = (shiftedHour - 2) / 4
|
|
1162
|
+
return 5 + 1 * Math.cos(Math.PI * t)
|
|
1163
|
+
} else {
|
|
1164
|
+
const t = (shiftedHour - 6) / 3
|
|
1165
|
+
return 4 - 3 * t
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
var PresenceChoreographer = class {
|
|
1169
|
+
config
|
|
1170
|
+
stats = {
|
|
1171
|
+
distractionPausesInjected: 0,
|
|
1172
|
+
offlineGapsInjected: 0,
|
|
1173
|
+
readReceiptsDelayed: 0,
|
|
1174
|
+
readReceiptsSkipped: 0,
|
|
1175
|
+
typingPlansComputed: 0,
|
|
1176
|
+
typingPlansExecuted: 0,
|
|
1177
|
+
totalTypingTimeMs: 0
|
|
1178
|
+
}
|
|
1179
|
+
constructor(config = {}) {
|
|
1180
|
+
this.config = {
|
|
1181
|
+
...DEFAULT_CONFIG7,
|
|
1182
|
+
...config,
|
|
1183
|
+
circadian: {
|
|
1184
|
+
...DEFAULT_CONFIG7.circadian,
|
|
1185
|
+
...(config.circadian || {})
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Get current activity factor (0.1 to 1.0).
|
|
1191
|
+
* Higher = more active = shorter delays.
|
|
1192
|
+
* If circadian disabled, returns 1.0.
|
|
1193
|
+
*/
|
|
1194
|
+
getCurrentActivityFactor() {
|
|
1195
|
+
if (!this.config.enabled || !this.config.enableCircadianRhythm) {
|
|
1196
|
+
return 1
|
|
1197
|
+
}
|
|
1198
|
+
const hour = this.getLocalHour()
|
|
1199
|
+
const curve = ACTIVITY_CURVES[this.config.activityCurve]
|
|
1200
|
+
return curve[hour] || 0.5
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Check if should pause for distraction.
|
|
1204
|
+
* Returns { pause: true, durationMs: 600000 } if probability check passes.
|
|
1205
|
+
*/
|
|
1206
|
+
shouldPauseForDistraction() {
|
|
1207
|
+
if (!this.config.enabled) {
|
|
1208
|
+
return { pause: false, durationMs: 0 }
|
|
1209
|
+
}
|
|
1210
|
+
if (Math.random() < this.config.distractionPauseProbability) {
|
|
1211
|
+
const durationMs = this.randomBetween(this.config.distractionPauseMinMs, this.config.distractionPauseMaxMs)
|
|
1212
|
+
this.stats.distractionPausesInjected++
|
|
1213
|
+
return { pause: true, durationMs }
|
|
1214
|
+
}
|
|
1215
|
+
return { pause: false, durationMs: 0 }
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Check if should take offline gap.
|
|
1219
|
+
* Returns { offline: true, durationMs: 600000 } if probability check passes.
|
|
1220
|
+
*/
|
|
1221
|
+
shouldTakeOfflineGap() {
|
|
1222
|
+
if (!this.config.enabled) {
|
|
1223
|
+
return { offline: false, durationMs: 0 }
|
|
1224
|
+
}
|
|
1225
|
+
if (Math.random() < this.config.offlineGapProbability) {
|
|
1226
|
+
const durationMs = this.randomBetween(this.config.offlineGapMinMs, this.config.offlineGapMaxMs)
|
|
1227
|
+
this.stats.offlineGapsInjected++
|
|
1228
|
+
return { offline: true, durationMs }
|
|
1229
|
+
}
|
|
1230
|
+
return { offline: false, durationMs: 0 }
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Check if should mark message as read.
|
|
1234
|
+
* Returns { mark: false } if skip probability hit.
|
|
1235
|
+
* Returns { mark: true, delayMs: 5000 } otherwise.
|
|
1236
|
+
* Applies circadian multiplier to delay.
|
|
1237
|
+
*/
|
|
1238
|
+
shouldMarkRead() {
|
|
1239
|
+
if (!this.config.enabled) {
|
|
1240
|
+
return { mark: true, delayMs: 0 }
|
|
1241
|
+
}
|
|
1242
|
+
if (Math.random() < this.config.readReceiptSkipProbability) {
|
|
1243
|
+
this.stats.readReceiptsSkipped++
|
|
1244
|
+
return { mark: false, delayMs: 0 }
|
|
1245
|
+
}
|
|
1246
|
+
const baseDelayMs = this.randomBetween(this.config.readReceiptDelayMinMs, this.config.readReceiptDelayMaxMs)
|
|
1247
|
+
let delayMs = baseDelayMs
|
|
1248
|
+
if (this.config.circadian.enabled) {
|
|
1249
|
+
const circadianMultiplier = getCircadianMultiplier(
|
|
1250
|
+
/* @__PURE__ */ new Date(),
|
|
1251
|
+
this.config.circadian.profile,
|
|
1252
|
+
this.config.circadian.timezone
|
|
1253
|
+
)
|
|
1254
|
+
delayMs = Math.floor(baseDelayMs * circadianMultiplier)
|
|
1255
|
+
}
|
|
1256
|
+
this.stats.readReceiptsDelayed++
|
|
1257
|
+
return { mark: true, delayMs }
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Compute realistic typing duration for a message of given length.
|
|
1261
|
+
* Includes Gaussian WPM variance + think-pause injection + circadian timing multiplier.
|
|
1262
|
+
* Returns a "typing plan": array of { state, durationMs } steps the caller should execute sequentially.
|
|
1263
|
+
*
|
|
1264
|
+
* plan = [
|
|
1265
|
+
* { state: 'composing', durationMs: 4200 },
|
|
1266
|
+
* { state: 'paused', durationMs: 950 }, // think pause
|
|
1267
|
+
* { state: 'composing', durationMs: 6800 },
|
|
1268
|
+
* { state: 'paused', durationMs: 600 }, // brief stop before send
|
|
1269
|
+
* ]
|
|
1270
|
+
*/
|
|
1271
|
+
computeTypingPlan(messageLength) {
|
|
1272
|
+
if (!this.config.enabled || !this.config.enableTypingModel) {
|
|
1273
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }]
|
|
1274
|
+
}
|
|
1275
|
+
this.stats.typingPlansComputed++
|
|
1276
|
+
if (messageLength === 0) {
|
|
1277
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }]
|
|
1278
|
+
}
|
|
1279
|
+
const wpmSample = this.clamp(this.gaussianSample(this.config.typingWPM, this.config.typingWPMStdDev), 10, 120)
|
|
1280
|
+
const cps = (wpmSample * 5) / 60
|
|
1281
|
+
const baseMs = (messageLength / cps) * 1e3
|
|
1282
|
+
let circadianMultiplier = 1
|
|
1283
|
+
if (this.config.circadian.enabled) {
|
|
1284
|
+
circadianMultiplier = getCircadianMultiplier(
|
|
1285
|
+
/* @__PURE__ */ new Date(),
|
|
1286
|
+
this.config.circadian.profile,
|
|
1287
|
+
this.config.circadian.timezone
|
|
1288
|
+
)
|
|
1289
|
+
}
|
|
1290
|
+
const targetMs = this.clamp(baseMs * circadianMultiplier, this.config.typingMinMs, this.config.typingMaxMs)
|
|
1291
|
+
const plan = []
|
|
1292
|
+
let remainingBudget = targetMs
|
|
1293
|
+
let position = 0
|
|
1294
|
+
const chunkSize = 10
|
|
1295
|
+
const numChunks = Math.max(1, Math.ceil(messageLength / chunkSize))
|
|
1296
|
+
for (let i = 0; i < numChunks && remainingBudget > 0; i++) {
|
|
1297
|
+
const charsInChunk = Math.min(chunkSize, messageLength - position)
|
|
1298
|
+
const remainingChunks = numChunks - i
|
|
1299
|
+
const chunkBudget = remainingBudget / remainingChunks
|
|
1300
|
+
const chunkTypingMs = Math.floor(Math.min(chunkBudget, remainingBudget))
|
|
1301
|
+
if (chunkTypingMs <= 0) break
|
|
1302
|
+
if (i > 0 && i < numChunks - 1 && Math.random() < this.config.thinkPauseProbability) {
|
|
1303
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs })
|
|
1304
|
+
remainingBudget -= chunkTypingMs
|
|
1305
|
+
const basePauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs)
|
|
1306
|
+
const pauseMs = Math.floor(basePauseMs * circadianMultiplier)
|
|
1307
|
+
plan.push({ state: 'paused', durationMs: pauseMs })
|
|
1308
|
+
} else {
|
|
1309
|
+
if (plan.length === 0 || plan[plan.length - 1].state === 'paused') {
|
|
1310
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs })
|
|
1311
|
+
} else {
|
|
1312
|
+
plan[plan.length - 1].durationMs += chunkTypingMs
|
|
1313
|
+
}
|
|
1314
|
+
remainingBudget -= chunkTypingMs
|
|
1315
|
+
}
|
|
1316
|
+
position += charsInChunk
|
|
1317
|
+
}
|
|
1318
|
+
if (Math.random() < this.config.intermittentPausedProbability) {
|
|
1319
|
+
const baseFinalPauseMs = this.randomBetween(200, 800)
|
|
1320
|
+
const finalPauseMs = Math.floor(baseFinalPauseMs * circadianMultiplier)
|
|
1321
|
+
plan.push({ state: 'paused', durationMs: finalPauseMs })
|
|
1322
|
+
}
|
|
1323
|
+
if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
|
|
1324
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }]
|
|
1325
|
+
}
|
|
1326
|
+
return plan
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Execute a typing plan against a Baileys-shaped sock with sendPresenceUpdate(state, jid).
|
|
1330
|
+
* Awaits each step's duration. Updates stats.
|
|
1331
|
+
*
|
|
1332
|
+
* await choreo.executeTypingPlan(sock, jid, plan);
|
|
1333
|
+
* await sock.sendMessage(jid, content);
|
|
1334
|
+
*/
|
|
1335
|
+
async executeTypingPlan(sock, jid, plan, options) {
|
|
1336
|
+
this.stats.typingPlansExecuted++
|
|
1337
|
+
for (const step of plan) {
|
|
1338
|
+
if (options?.signal?.aborted) {
|
|
1339
|
+
await Promise.resolve(sock.sendPresenceUpdate('paused', jid))
|
|
1340
|
+
throw new Error('Typing plan aborted')
|
|
1341
|
+
}
|
|
1342
|
+
await Promise.resolve(sock.sendPresenceUpdate(step.state, jid))
|
|
1343
|
+
await this.sleep(step.durationMs)
|
|
1344
|
+
this.stats.totalTypingTimeMs += step.durationMs
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Get statistics.
|
|
1349
|
+
*/
|
|
1350
|
+
getStats() {
|
|
1351
|
+
return {
|
|
1352
|
+
currentActivityFactor: this.getCurrentActivityFactor(),
|
|
1353
|
+
distractionPausesInjected: this.stats.distractionPausesInjected,
|
|
1354
|
+
offlineGapsInjected: this.stats.offlineGapsInjected,
|
|
1355
|
+
readReceiptsDelayed: this.stats.readReceiptsDelayed,
|
|
1356
|
+
readReceiptsSkipped: this.stats.readReceiptsSkipped,
|
|
1357
|
+
currentHourLocal: this.getLocalHour(),
|
|
1358
|
+
typingPlansComputed: this.stats.typingPlansComputed,
|
|
1359
|
+
typingPlansExecuted: this.stats.typingPlansExecuted,
|
|
1360
|
+
totalTypingTimeMs: this.stats.totalTypingTimeMs
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Reset statistics.
|
|
1365
|
+
*/
|
|
1366
|
+
reset() {
|
|
1367
|
+
this.stats = {
|
|
1368
|
+
distractionPausesInjected: 0,
|
|
1369
|
+
offlineGapsInjected: 0,
|
|
1370
|
+
readReceiptsDelayed: 0,
|
|
1371
|
+
readReceiptsSkipped: 0,
|
|
1372
|
+
typingPlansComputed: 0,
|
|
1373
|
+
typingPlansExecuted: 0,
|
|
1374
|
+
totalTypingTimeMs: 0
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
// Private helpers
|
|
1378
|
+
getLocalHour() {
|
|
1379
|
+
try {
|
|
1380
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
1381
|
+
timeZone: this.config.timezone,
|
|
1382
|
+
hour: 'numeric',
|
|
1383
|
+
hour12: false
|
|
1384
|
+
})
|
|
1385
|
+
const parts = formatter.formatToParts(/* @__PURE__ */ new Date())
|
|
1386
|
+
const hourPart = parts.find(p => p.type === 'hour')
|
|
1387
|
+
if (hourPart) {
|
|
1388
|
+
return parseInt(hourPart.value, 10)
|
|
1389
|
+
}
|
|
1390
|
+
} catch (error) {}
|
|
1391
|
+
return /* @__PURE__ */ new Date().getUTCHours()
|
|
1392
|
+
}
|
|
1393
|
+
randomBetween(min, max) {
|
|
1394
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
1395
|
+
}
|
|
1396
|
+
clamp(value, min, max) {
|
|
1397
|
+
return Math.max(min, Math.min(max, value))
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Generate Gaussian sample using Box-Muller transform.
|
|
1401
|
+
* Returns a sample from N(mean, stdDev).
|
|
1402
|
+
*/
|
|
1403
|
+
gaussianSample(mean, stdDev) {
|
|
1404
|
+
const u1 = Math.random()
|
|
1405
|
+
const u2 = Math.random()
|
|
1406
|
+
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
|
|
1407
|
+
return mean + z0 * stdDev
|
|
1408
|
+
}
|
|
1409
|
+
sleep(ms) {
|
|
1410
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// retryTracker.js
|
|
1415
|
+
var DEFAULT_CONFIG8 = {
|
|
1416
|
+
enabled: false,
|
|
1417
|
+
maxRetries: 5,
|
|
1418
|
+
spiralThreshold: 3,
|
|
1419
|
+
onSpiral: () => {}
|
|
1420
|
+
}
|
|
1421
|
+
var RetryReasonTracker = class {
|
|
1422
|
+
config
|
|
1423
|
+
retries = /* @__PURE__ */ new Map()
|
|
1424
|
+
totalRetries = 0
|
|
1425
|
+
reasonCounts = {
|
|
1426
|
+
no_session: 0,
|
|
1427
|
+
invalid_key: 0,
|
|
1428
|
+
bad_mac: 0,
|
|
1429
|
+
decryption_failure: 0,
|
|
1430
|
+
server_error_463: 0,
|
|
1431
|
+
server_error_429: 0,
|
|
1432
|
+
timeout: 0,
|
|
1433
|
+
no_route: 0,
|
|
1434
|
+
node_malformed: 0,
|
|
1435
|
+
unknown: 0
|
|
1436
|
+
}
|
|
1437
|
+
spiralsDetected = 0
|
|
1438
|
+
constructor(config) {
|
|
1439
|
+
this.config = { ...DEFAULT_CONFIG8, ...config }
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Call when a messages.update event arrives with a status/error.
|
|
1443
|
+
* Classifies and records the retry.
|
|
1444
|
+
*/
|
|
1445
|
+
onMessageUpdate(update) {
|
|
1446
|
+
if (!this.config.enabled) return
|
|
1447
|
+
const msgId = update.key?.id
|
|
1448
|
+
if (!msgId) return
|
|
1449
|
+
if (update.status !== 0 && !update.error) return
|
|
1450
|
+
const reason = this.classify(update.error || update)
|
|
1451
|
+
this.recordRetry(msgId, reason)
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Classify an arbitrary error object into a RetryReason
|
|
1455
|
+
*/
|
|
1456
|
+
classify(err) {
|
|
1457
|
+
if (!err) return 'unknown'
|
|
1458
|
+
const statusCode = err.output?.statusCode || err.statusCode || err.status
|
|
1459
|
+
if (statusCode === 463) return 'server_error_463'
|
|
1460
|
+
if (statusCode === 429) return 'server_error_429'
|
|
1461
|
+
const errorMsg = (err.message || err.text || String(err)).toLowerCase()
|
|
1462
|
+
if (errorMsg.includes('bad mac')) return 'bad_mac'
|
|
1463
|
+
if (errorMsg.includes('no session') || errorMsg.includes('session not found')) return 'no_session'
|
|
1464
|
+
if (errorMsg.includes('invalid key') || errorMsg.includes('key error')) return 'invalid_key'
|
|
1465
|
+
if (errorMsg.includes('decryption') || errorMsg.includes('decrypt')) return 'decryption_failure'
|
|
1466
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) return 'timeout'
|
|
1467
|
+
if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
|
|
1468
|
+
return 'no_route'
|
|
1469
|
+
if (errorMsg.includes('malformed') || errorMsg.includes('invalid node')) return 'node_malformed'
|
|
1470
|
+
return 'unknown'
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Record a retry for a message
|
|
1474
|
+
*/
|
|
1475
|
+
recordRetry(msgId, reason) {
|
|
1476
|
+
const now = Date.now()
|
|
1477
|
+
let record = this.retries.get(msgId)
|
|
1478
|
+
if (!record) {
|
|
1479
|
+
record = {
|
|
1480
|
+
msgId,
|
|
1481
|
+
count: 0,
|
|
1482
|
+
reasons: [],
|
|
1483
|
+
firstRetry: now,
|
|
1484
|
+
lastRetry: now
|
|
1485
|
+
}
|
|
1486
|
+
this.retries.set(msgId, record)
|
|
1487
|
+
}
|
|
1488
|
+
record.count++
|
|
1489
|
+
record.reasons.push(reason)
|
|
1490
|
+
record.lastRetry = now
|
|
1491
|
+
this.totalRetries++
|
|
1492
|
+
this.reasonCounts[reason]++
|
|
1493
|
+
if (record.count >= this.config.spiralThreshold) {
|
|
1494
|
+
this.spiralsDetected++
|
|
1495
|
+
this.config.onSpiral(msgId, reason)
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Should we warn the user this message is spiraling?
|
|
1500
|
+
*/
|
|
1501
|
+
isSpiraling(msgId) {
|
|
1502
|
+
const record = this.retries.get(msgId)
|
|
1503
|
+
return record ? record.count >= this.config.spiralThreshold : false
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Reset counters for a specific message (call on successful delivery)
|
|
1507
|
+
*/
|
|
1508
|
+
clear(msgId) {
|
|
1509
|
+
this.retries.delete(msgId)
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Get current stats
|
|
1513
|
+
*/
|
|
1514
|
+
getStats() {
|
|
1515
|
+
return {
|
|
1516
|
+
totalRetries: this.totalRetries,
|
|
1517
|
+
byReason: { ...this.reasonCounts },
|
|
1518
|
+
spiralsDetected: this.spiralsDetected,
|
|
1519
|
+
activeRetries: this.retries.size
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Clean up old retry records (>5 minutes old)
|
|
1524
|
+
*/
|
|
1525
|
+
cleanup() {
|
|
1526
|
+
const now = Date.now()
|
|
1527
|
+
const maxAge = 5 * 60 * 1e3
|
|
1528
|
+
for (const [msgId, record] of this.retries.entries()) {
|
|
1529
|
+
if (now - record.lastRetry > maxAge) {
|
|
1530
|
+
this.retries.delete(msgId)
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Destroy and clean up
|
|
1536
|
+
*/
|
|
1537
|
+
destroy() {
|
|
1538
|
+
this.retries.clear()
|
|
1539
|
+
this.cleanup()
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// reconnectThrottle.js
|
|
1544
|
+
var DEFAULT_CONFIG9 = {
|
|
1545
|
+
enabled: false,
|
|
1546
|
+
rampDurationMs: 6e4,
|
|
1547
|
+
initialRateMultiplier: 0.1,
|
|
1548
|
+
rampSteps: 6,
|
|
1549
|
+
baselineRatePerMinute: null
|
|
1550
|
+
}
|
|
1551
|
+
var PostReconnectThrottle = class {
|
|
1552
|
+
config
|
|
1553
|
+
throttledSince = null
|
|
1554
|
+
throttledSendCount = 0
|
|
1555
|
+
lifetimeReconnects = 0
|
|
1556
|
+
rampTimer = null
|
|
1557
|
+
currentStep = 0
|
|
1558
|
+
// Tracking sends in current window
|
|
1559
|
+
sendsInCurrentWindow = 0
|
|
1560
|
+
currentWindowStart = 0
|
|
1561
|
+
WINDOW_DURATION_MS = 6e4
|
|
1562
|
+
// 1 minute window
|
|
1563
|
+
constructor(config) {
|
|
1564
|
+
this.config = {
|
|
1565
|
+
...DEFAULT_CONFIG9,
|
|
1566
|
+
...config,
|
|
1567
|
+
baselineRatePerMinute: config?.baselineRatePerMinute || null
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Call when connection is re-established. Starts throttle window.
|
|
1572
|
+
*/
|
|
1573
|
+
onReconnect() {
|
|
1574
|
+
if (!this.config.enabled) return
|
|
1575
|
+
this.throttledSince = Date.now()
|
|
1576
|
+
this.currentStep = 0
|
|
1577
|
+
this.throttledSendCount = 0
|
|
1578
|
+
this.lifetimeReconnects++
|
|
1579
|
+
this.sendsInCurrentWindow = 0
|
|
1580
|
+
this.currentWindowStart = Date.now()
|
|
1581
|
+
if (this.rampTimer) {
|
|
1582
|
+
clearTimeout(this.rampTimer)
|
|
1583
|
+
}
|
|
1584
|
+
this.scheduleNextRampStep()
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Call when connection drops (optional — reset state).
|
|
1588
|
+
*/
|
|
1589
|
+
onDisconnect() {}
|
|
1590
|
+
/**
|
|
1591
|
+
* Schedule the next ramp step
|
|
1592
|
+
*/
|
|
1593
|
+
scheduleNextRampStep() {
|
|
1594
|
+
if (this.currentStep >= this.config.rampSteps) {
|
|
1595
|
+
this.throttledSince = null
|
|
1596
|
+
this.rampTimer = null
|
|
1597
|
+
return
|
|
1598
|
+
}
|
|
1599
|
+
const stepDuration = this.config.rampDurationMs / this.config.rampSteps
|
|
1600
|
+
this.rampTimer = setTimeout(() => {
|
|
1601
|
+
this.currentStep++
|
|
1602
|
+
this.scheduleNextRampStep()
|
|
1603
|
+
}, stepDuration)
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Returns current rate multiplier (1.0 = no throttle)
|
|
1607
|
+
*/
|
|
1608
|
+
getCurrentMultiplier() {
|
|
1609
|
+
if (!this.config.enabled || !this.throttledSince) {
|
|
1610
|
+
return 1
|
|
1611
|
+
}
|
|
1612
|
+
const elapsed = Date.now() - this.throttledSince
|
|
1613
|
+
if (elapsed >= this.config.rampDurationMs) {
|
|
1614
|
+
return 1
|
|
1615
|
+
}
|
|
1616
|
+
const progress = this.currentStep / this.config.rampSteps
|
|
1617
|
+
const multiplier = this.config.initialRateMultiplier + (1 - this.config.initialRateMultiplier) * progress
|
|
1618
|
+
return Math.min(1, multiplier)
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Checks if a send should be gated. Returns {allowed, reason, retryAfterMs?}
|
|
1622
|
+
*/
|
|
1623
|
+
beforeSend() {
|
|
1624
|
+
if (!this.config.enabled || !this.throttledSince) {
|
|
1625
|
+
return { allowed: true }
|
|
1626
|
+
}
|
|
1627
|
+
const now = Date.now()
|
|
1628
|
+
const multiplier = this.getCurrentMultiplier()
|
|
1629
|
+
if (multiplier >= 1) {
|
|
1630
|
+
this.throttledSince = null
|
|
1631
|
+
return { allowed: true }
|
|
1632
|
+
}
|
|
1633
|
+
if (now - this.currentWindowStart >= this.WINDOW_DURATION_MS) {
|
|
1634
|
+
this.sendsInCurrentWindow = 0
|
|
1635
|
+
this.currentWindowStart = now
|
|
1636
|
+
}
|
|
1637
|
+
const baselineRate = this.config.baselineRatePerMinute ? this.config.baselineRatePerMinute() : 8
|
|
1638
|
+
const allowedInWindow = Math.max(1, Math.floor(baselineRate * multiplier))
|
|
1639
|
+
if (this.sendsInCurrentWindow >= allowedInWindow) {
|
|
1640
|
+
const windowRemaining = this.WINDOW_DURATION_MS - (now - this.currentWindowStart)
|
|
1641
|
+
return {
|
|
1642
|
+
allowed: false,
|
|
1643
|
+
reason: `Post-reconnect throttle: ${Math.floor(multiplier * 100)}% rate (${this.sendsInCurrentWindow}/${allowedInWindow} sends in window)`,
|
|
1644
|
+
retryAfterMs: windowRemaining
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
this.sendsInCurrentWindow++
|
|
1648
|
+
this.throttledSendCount++
|
|
1649
|
+
return { allowed: true }
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Get current stats
|
|
1653
|
+
*/
|
|
1654
|
+
getStats() {
|
|
1655
|
+
const multiplier = this.getCurrentMultiplier()
|
|
1656
|
+
const isThrottled = this.throttledSince !== null && multiplier < 1
|
|
1657
|
+
const remainingMs =
|
|
1658
|
+
isThrottled && this.throttledSince
|
|
1659
|
+
? Math.max(0, this.config.rampDurationMs - (Date.now() - this.throttledSince))
|
|
1660
|
+
: 0
|
|
1661
|
+
return {
|
|
1662
|
+
isThrottled,
|
|
1663
|
+
currentMultiplier: multiplier,
|
|
1664
|
+
throttledSinceMs: this.throttledSince,
|
|
1665
|
+
remainingMs,
|
|
1666
|
+
throttledSendCount: this.throttledSendCount,
|
|
1667
|
+
lifetimeReconnects: this.lifetimeReconnects
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Destroy and clean up timers
|
|
1672
|
+
*/
|
|
1673
|
+
destroy() {
|
|
1674
|
+
if (this.rampTimer) {
|
|
1675
|
+
clearTimeout(this.rampTimer)
|
|
1676
|
+
this.rampTimer = null
|
|
1677
|
+
}
|
|
1678
|
+
this.throttledSince = null
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// lidResolver.js
|
|
1683
|
+
var DEFAULT_CONFIG10 = {
|
|
1684
|
+
canonical: 'pn',
|
|
1685
|
+
maxEntries: 1e4
|
|
1686
|
+
}
|
|
1687
|
+
var LidResolver = class {
|
|
1688
|
+
config
|
|
1689
|
+
persistence
|
|
1690
|
+
// Bidirectional maps: lid→pn and pn→lid
|
|
1691
|
+
lidToPn = /* @__PURE__ */ new Map()
|
|
1692
|
+
pnToLid = /* @__PURE__ */ new Map()
|
|
1693
|
+
// pn → lid (for quick reverse lookup)
|
|
1694
|
+
stats = {
|
|
1695
|
+
learnedFromEvents: 0,
|
|
1696
|
+
lookupsServed: 0,
|
|
1697
|
+
lookupMisses: 0
|
|
1698
|
+
}
|
|
1699
|
+
constructor(config = {}) {
|
|
1700
|
+
this.config = { ...DEFAULT_CONFIG10, ...config }
|
|
1701
|
+
this.persistence = config.persistence
|
|
1702
|
+
if (this.persistence?.load) {
|
|
1703
|
+
void this.hydrate()
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Learn from a message event. Idempotent.
|
|
1708
|
+
* Accepts partial mappings — will use whatever fields are available.
|
|
1709
|
+
*/
|
|
1710
|
+
learn(mapping) {
|
|
1711
|
+
let lid = mapping.lid ? this.normalizeJid(mapping.lid) : void 0
|
|
1712
|
+
let pn = mapping.pn ? this.normalizeJid(mapping.pn) : void 0
|
|
1713
|
+
const phone = mapping.phone
|
|
1714
|
+
if (!lid || (!pn && !phone)) {
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1717
|
+
if (!pn && phone) {
|
|
1718
|
+
pn = `${phone}@s.whatsapp.net`
|
|
1719
|
+
}
|
|
1720
|
+
if (!lid || !pn) return
|
|
1721
|
+
if (!lid.endsWith('@lid')) return
|
|
1722
|
+
if (!pn.endsWith('@s.whatsapp.net')) return
|
|
1723
|
+
const existing = this.lidToPn.get(lid)
|
|
1724
|
+
if (existing) {
|
|
1725
|
+
existing.seenCount++
|
|
1726
|
+
existing.learnedAt = Date.now()
|
|
1727
|
+
return
|
|
1728
|
+
}
|
|
1729
|
+
const extractedPhone = phone || pn.split('@')[0]
|
|
1730
|
+
const newMapping = {
|
|
1731
|
+
lid,
|
|
1732
|
+
pn,
|
|
1733
|
+
phone: extractedPhone,
|
|
1734
|
+
learnedAt: Date.now(),
|
|
1735
|
+
seenCount: 1
|
|
1736
|
+
}
|
|
1737
|
+
if (this.lidToPn.size >= this.config.maxEntries) {
|
|
1738
|
+
this.evictLRU()
|
|
1739
|
+
}
|
|
1740
|
+
this.lidToPn.set(lid, newMapping)
|
|
1741
|
+
this.pnToLid.set(pn, lid)
|
|
1742
|
+
this.stats.learnedFromEvents++
|
|
1743
|
+
if (this.persistence?.save) {
|
|
1744
|
+
void this.flush()
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Given any form (LID or PN), return the canonical form.
|
|
1749
|
+
* Falls back to input if unknown (no throw).
|
|
1750
|
+
*/
|
|
1751
|
+
resolveCanonical(jid) {
|
|
1752
|
+
const normalized = this.normalizeJid(jid)
|
|
1753
|
+
if (this.config.canonical === 'pn') {
|
|
1754
|
+
if (normalized.endsWith('@lid')) {
|
|
1755
|
+
const mapping = this.lidToPn.get(normalized)
|
|
1756
|
+
if (mapping) {
|
|
1757
|
+
this.stats.lookupsServed++
|
|
1758
|
+
mapping.learnedAt = Date.now()
|
|
1759
|
+
return mapping.pn
|
|
1760
|
+
}
|
|
1761
|
+
this.stats.lookupMisses++
|
|
1762
|
+
return jid
|
|
1763
|
+
}
|
|
1764
|
+
this.stats.lookupsServed++
|
|
1765
|
+
return normalized
|
|
1766
|
+
} else {
|
|
1767
|
+
if (normalized.endsWith('@s.whatsapp.net')) {
|
|
1768
|
+
const lid = this.pnToLid.get(normalized)
|
|
1769
|
+
if (lid) {
|
|
1770
|
+
this.stats.lookupsServed++
|
|
1771
|
+
const mapping = this.lidToPn.get(lid)
|
|
1772
|
+
if (mapping) {
|
|
1773
|
+
mapping.learnedAt = Date.now()
|
|
1774
|
+
}
|
|
1775
|
+
return lid
|
|
1776
|
+
}
|
|
1777
|
+
this.stats.lookupMisses++
|
|
1778
|
+
return jid
|
|
1779
|
+
}
|
|
1780
|
+
this.stats.lookupsServed++
|
|
1781
|
+
return normalized
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Lookup partner form. Returns null if unknown.
|
|
1786
|
+
*/
|
|
1787
|
+
getLid(pn) {
|
|
1788
|
+
const normalized = this.normalizeJid(pn)
|
|
1789
|
+
const lid = this.pnToLid.get(normalized)
|
|
1790
|
+
if (lid) {
|
|
1791
|
+
const mapping = this.lidToPn.get(lid)
|
|
1792
|
+
if (mapping) {
|
|
1793
|
+
mapping.learnedAt = Date.now()
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return lid || null
|
|
1797
|
+
}
|
|
1798
|
+
getPn(lid) {
|
|
1799
|
+
const normalized = this.normalizeJid(lid)
|
|
1800
|
+
const mapping = this.lidToPn.get(normalized)
|
|
1801
|
+
if (mapping) {
|
|
1802
|
+
mapping.learnedAt = Date.now()
|
|
1803
|
+
return mapping.pn
|
|
1804
|
+
}
|
|
1805
|
+
return null
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Full mapping for inspection
|
|
1809
|
+
*/
|
|
1810
|
+
getMapping(jid) {
|
|
1811
|
+
const normalized = this.normalizeJid(jid)
|
|
1812
|
+
const byLid = this.lidToPn.get(normalized)
|
|
1813
|
+
if (byLid) {
|
|
1814
|
+
byLid.learnedAt = Date.now()
|
|
1815
|
+
return byLid
|
|
1816
|
+
}
|
|
1817
|
+
const lid = this.pnToLid.get(normalized)
|
|
1818
|
+
if (lid) {
|
|
1819
|
+
const mapping = this.lidToPn.get(lid)
|
|
1820
|
+
if (mapping) {
|
|
1821
|
+
mapping.learnedAt = Date.now()
|
|
1822
|
+
return mapping
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return null
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Seed from persistence (called automatically in constructor if persistence provided)
|
|
1829
|
+
*/
|
|
1830
|
+
async hydrate() {
|
|
1831
|
+
if (!this.persistence?.load) return
|
|
1832
|
+
try {
|
|
1833
|
+
const stored = await this.persistence.load()
|
|
1834
|
+
if (!stored || typeof stored !== 'object') return
|
|
1835
|
+
for (const [lid, serialized] of Object.entries(stored)) {
|
|
1836
|
+
if (typeof serialized === 'string') {
|
|
1837
|
+
const pn = serialized
|
|
1838
|
+
const phone = pn.split('@')[0]
|
|
1839
|
+
const mapping = {
|
|
1840
|
+
lid,
|
|
1841
|
+
pn,
|
|
1842
|
+
phone,
|
|
1843
|
+
learnedAt: Date.now(),
|
|
1844
|
+
seenCount: 1
|
|
1845
|
+
}
|
|
1846
|
+
this.lidToPn.set(lid, mapping)
|
|
1847
|
+
this.pnToLid.set(pn, lid)
|
|
1848
|
+
} else if (typeof serialized === 'object' && serialized !== null) {
|
|
1849
|
+
const mapping = serialized
|
|
1850
|
+
this.lidToPn.set(lid, mapping)
|
|
1851
|
+
this.pnToLid.set(mapping.pn, lid)
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} catch (error) {}
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Flush current map to persistence
|
|
1858
|
+
*/
|
|
1859
|
+
async flush() {
|
|
1860
|
+
if (!this.persistence?.save) return
|
|
1861
|
+
try {
|
|
1862
|
+
const toStore = {}
|
|
1863
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
1864
|
+
toStore[lid] = mapping
|
|
1865
|
+
}
|
|
1866
|
+
await this.persistence.save(toStore)
|
|
1867
|
+
} catch (error) {}
|
|
1868
|
+
}
|
|
1869
|
+
getStats() {
|
|
1870
|
+
return {
|
|
1871
|
+
totalMappings: this.lidToPn.size,
|
|
1872
|
+
learnedFromEvents: this.stats.learnedFromEvents,
|
|
1873
|
+
lookupsServed: this.stats.lookupsServed,
|
|
1874
|
+
lookupMisses: this.stats.lookupMisses,
|
|
1875
|
+
canonicalForm: this.config.canonical
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Clear everything
|
|
1880
|
+
*/
|
|
1881
|
+
reset() {
|
|
1882
|
+
this.lidToPn.clear()
|
|
1883
|
+
this.pnToLid.clear()
|
|
1884
|
+
this.stats = {
|
|
1885
|
+
learnedFromEvents: 0,
|
|
1886
|
+
lookupsServed: 0,
|
|
1887
|
+
lookupMisses: 0
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
destroy() {
|
|
1891
|
+
this.reset()
|
|
1892
|
+
if (this.persistence?.save) {
|
|
1893
|
+
void this.flush()
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// Private helpers
|
|
1897
|
+
/**
|
|
1898
|
+
* Normalize JID: strip device suffix `:N`
|
|
1899
|
+
*/
|
|
1900
|
+
normalizeJid(jid) {
|
|
1901
|
+
return jid.replace(/:\d+@/, '@')
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Evict least recently accessed mapping (LRU)
|
|
1905
|
+
*/
|
|
1906
|
+
evictLRU() {
|
|
1907
|
+
let oldestLid = null
|
|
1908
|
+
let oldestTime = Infinity
|
|
1909
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
1910
|
+
if (mapping.learnedAt < oldestTime) {
|
|
1911
|
+
oldestTime = mapping.learnedAt
|
|
1912
|
+
oldestLid = lid
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (oldestLid) {
|
|
1916
|
+
const mapping = this.lidToPn.get(oldestLid)
|
|
1917
|
+
if (mapping) {
|
|
1918
|
+
this.pnToLid.delete(mapping.pn)
|
|
1919
|
+
}
|
|
1920
|
+
this.lidToPn.delete(oldestLid)
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// jidCanonicalizer.js
|
|
1926
|
+
var DEFAULT_CONFIG11 = {
|
|
1927
|
+
enabled: false,
|
|
1928
|
+
canonicalizeOutbound: true,
|
|
1929
|
+
learnFromEvents: true
|
|
1930
|
+
}
|
|
1931
|
+
var JidCanonicalizer = class {
|
|
1932
|
+
config
|
|
1933
|
+
lidResolver
|
|
1934
|
+
ownsResolver
|
|
1935
|
+
// Track if we created the resolver (for destroy)
|
|
1936
|
+
stats = {
|
|
1937
|
+
outboundCanonicalized: 0,
|
|
1938
|
+
outboundPassthrough: 0,
|
|
1939
|
+
inboundLearned: 0,
|
|
1940
|
+
canonicalKeyHits: 0,
|
|
1941
|
+
canonicalKeyMisses: 0
|
|
1942
|
+
}
|
|
1943
|
+
constructor(config = {}) {
|
|
1944
|
+
this.config = { ...DEFAULT_CONFIG11, ...config }
|
|
1945
|
+
if (config.resolver) {
|
|
1946
|
+
this.lidResolver = config.resolver
|
|
1947
|
+
this.ownsResolver = false
|
|
1948
|
+
} else {
|
|
1949
|
+
this.lidResolver = new LidResolver(config.resolverConfig)
|
|
1950
|
+
this.ownsResolver = true
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Access the underlying resolver (for cross-module sharing)
|
|
1955
|
+
*/
|
|
1956
|
+
get resolver() {
|
|
1957
|
+
return this.lidResolver
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Called by wrapper on every outbound send. Returns canonical JID.
|
|
1961
|
+
*/
|
|
1962
|
+
canonicalizeTarget(jid) {
|
|
1963
|
+
if (!this.config.enabled || !this.config.canonicalizeOutbound) {
|
|
1964
|
+
return jid
|
|
1965
|
+
}
|
|
1966
|
+
const canonical = this.lidResolver.resolveCanonical(jid)
|
|
1967
|
+
if (canonical !== jid) {
|
|
1968
|
+
this.stats.outboundCanonicalized++
|
|
1969
|
+
} else {
|
|
1970
|
+
this.stats.outboundPassthrough++
|
|
1971
|
+
}
|
|
1972
|
+
return canonical
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Returns a stable, canonical thread key for storage / DB indexing.
|
|
1976
|
+
*
|
|
1977
|
+
* Different from `canonicalizeTarget()` (which picks the right send target):
|
|
1978
|
+
* - canonicalizeTarget('1234@lid') → '+27...@s.whatsapp.net' (best send target)
|
|
1979
|
+
* - canonicalKey('1234@lid') → 'thread:27...' (stable thread identifier)
|
|
1980
|
+
*
|
|
1981
|
+
* If LID has known PN mapping → use phone-number form
|
|
1982
|
+
* If only LID known → use LID stripped of suffix
|
|
1983
|
+
* Always lowercase, no @-suffix, prefixed with `thread:`
|
|
1984
|
+
*
|
|
1985
|
+
* Apps using this as their DB key won't double-thread on LID/PN drift.
|
|
1986
|
+
*
|
|
1987
|
+
* @param jid - WhatsApp JID (can be PN, LID, group, or broadcast)
|
|
1988
|
+
* @returns Stable thread key for DB indexing
|
|
1989
|
+
*/
|
|
1990
|
+
canonicalKey(jid) {
|
|
1991
|
+
if (!jid || typeof jid !== 'string' || jid.trim() === '') {
|
|
1992
|
+
return 'thread:invalid'
|
|
1993
|
+
}
|
|
1994
|
+
const normalized = jid.trim().toLowerCase()
|
|
1995
|
+
const atIndex = normalized.indexOf('@')
|
|
1996
|
+
if (atIndex === -1) {
|
|
1997
|
+
return 'thread:invalid'
|
|
1998
|
+
}
|
|
1999
|
+
const user = normalized.substring(0, atIndex)
|
|
2000
|
+
const domain = normalized.substring(atIndex + 1)
|
|
2001
|
+
if (domain === 'g.us') {
|
|
2002
|
+
return `thread:group:${user}`
|
|
2003
|
+
}
|
|
2004
|
+
if (domain === 'broadcast') {
|
|
2005
|
+
return `thread:broadcast:${user}`
|
|
2006
|
+
}
|
|
2007
|
+
if (domain === 'newsletter') {
|
|
2008
|
+
return `thread:newsletter:${user}`
|
|
2009
|
+
}
|
|
2010
|
+
if (domain === 's.whatsapp.net') {
|
|
2011
|
+
this.stats.canonicalKeyHits++
|
|
2012
|
+
return `thread:${user}`
|
|
2013
|
+
}
|
|
2014
|
+
if (domain === 'lid') {
|
|
2015
|
+
const mapping = this.lidResolver.getMapping(normalized)
|
|
2016
|
+
if (mapping?.pn) {
|
|
2017
|
+
const pnUser = mapping.pn.split('@')[0]
|
|
2018
|
+
this.stats.canonicalKeyHits++
|
|
2019
|
+
return `thread:${pnUser}`
|
|
2020
|
+
} else {
|
|
2021
|
+
this.stats.canonicalKeyMisses++
|
|
2022
|
+
return `thread:lid:${user}`
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return `thread:${domain}:${user}`
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Called by wrapper on messages.upsert event. Learns mappings.
|
|
2029
|
+
*/
|
|
2030
|
+
onIncomingEvent(upsert) {
|
|
2031
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
2032
|
+
return
|
|
2033
|
+
}
|
|
2034
|
+
for (const msg of upsert.messages || []) {
|
|
2035
|
+
this.learnFromMessage(msg)
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Called by wrapper on messages.update event. Learns from sent-message refs.
|
|
2040
|
+
*/
|
|
2041
|
+
onMessageUpdate(updates) {
|
|
2042
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
2043
|
+
return
|
|
2044
|
+
}
|
|
2045
|
+
for (const update of updates) {
|
|
2046
|
+
if (update.key) {
|
|
2047
|
+
this.learnFromMessageKey(update.key)
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
getStats() {
|
|
2052
|
+
return {
|
|
2053
|
+
resolver: this.lidResolver.getStats(),
|
|
2054
|
+
outboundCanonicalized: this.stats.outboundCanonicalized,
|
|
2055
|
+
outboundPassthrough: this.stats.outboundPassthrough,
|
|
2056
|
+
inboundLearned: this.stats.inboundLearned,
|
|
2057
|
+
canonicalKeyHits: this.stats.canonicalKeyHits,
|
|
2058
|
+
canonicalKeyMisses: this.stats.canonicalKeyMisses
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
destroy() {
|
|
2062
|
+
if (this.ownsResolver) {
|
|
2063
|
+
this.lidResolver.destroy()
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
// Private helpers
|
|
2067
|
+
/**
|
|
2068
|
+
* Extract LID↔PN mappings from a message object
|
|
2069
|
+
*/
|
|
2070
|
+
learnFromMessage(msg) {
|
|
2071
|
+
if (!msg.key) return
|
|
2072
|
+
this.learnFromMessageKey(msg.key)
|
|
2073
|
+
if (msg.participantPn && msg.key.participant) {
|
|
2074
|
+
this.lidResolver.learn({
|
|
2075
|
+
lid: msg.key.participant.endsWith('@lid') ? msg.key.participant : void 0,
|
|
2076
|
+
pn: msg.participantPn
|
|
2077
|
+
})
|
|
2078
|
+
this.stats.inboundLearned++
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Extract mappings from message.key
|
|
2083
|
+
*/
|
|
2084
|
+
learnFromMessageKey(key) {
|
|
2085
|
+
if (!key) return
|
|
2086
|
+
if (key.participant && key.participantPn) {
|
|
2087
|
+
if (key.participant.endsWith('@lid')) {
|
|
2088
|
+
this.lidResolver.learn({
|
|
2089
|
+
lid: key.participant,
|
|
2090
|
+
pn: key.participantPn
|
|
2091
|
+
})
|
|
2092
|
+
this.stats.inboundLearned++
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (key.remoteJid && key.senderPn) {
|
|
2096
|
+
if (key.remoteJid.endsWith('@lid')) {
|
|
2097
|
+
this.lidResolver.learn({
|
|
2098
|
+
lid: key.remoteJid,
|
|
2099
|
+
pn: key.senderPn
|
|
2100
|
+
})
|
|
2101
|
+
this.stats.inboundLearned++
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (key.participant && key.remoteJid) {
|
|
2105
|
+
if (key.participant.endsWith('@s.whatsapp.net') && key.remoteJid.endsWith('@lid')) {
|
|
2106
|
+
this.lidResolver.learn({
|
|
2107
|
+
lid: key.remoteJid,
|
|
2108
|
+
pn: key.participant
|
|
2109
|
+
})
|
|
2110
|
+
this.stats.inboundLearned++
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// sessionStability.js
|
|
2117
|
+
function classifyDisconnect(statusCode) {
|
|
2118
|
+
if (statusCode === 401 || statusCode === 440) {
|
|
2119
|
+
return {
|
|
2120
|
+
category: 'fatal',
|
|
2121
|
+
shouldReconnect: false,
|
|
2122
|
+
message: 'Logged out \u2014 restart with QR code required',
|
|
2123
|
+
code: statusCode
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
if (statusCode === 515) {
|
|
2127
|
+
return {
|
|
2128
|
+
category: 'fatal',
|
|
2129
|
+
shouldReconnect: false,
|
|
2130
|
+
message: 'Restart required by WhatsApp \u2014 client too old or protocol mismatch',
|
|
2131
|
+
code: statusCode
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (statusCode === 405) {
|
|
2135
|
+
return {
|
|
2136
|
+
category: 'fatal',
|
|
2137
|
+
shouldReconnect: false,
|
|
2138
|
+
message: 'Method not allowed \u2014 server rejected connection method',
|
|
2139
|
+
code: statusCode
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (statusCode === 409 || statusCode === 428) {
|
|
2143
|
+
return {
|
|
2144
|
+
category: 'fatal',
|
|
2145
|
+
shouldReconnect: false,
|
|
2146
|
+
message: 'Connection replaced \u2014 another device took over',
|
|
2147
|
+
code: statusCode
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
if (statusCode === 412) {
|
|
2151
|
+
return {
|
|
2152
|
+
category: 'recoverable',
|
|
2153
|
+
shouldReconnect: true,
|
|
2154
|
+
backoffMs: 3e4,
|
|
2155
|
+
// 30 seconds
|
|
2156
|
+
message: 'Precondition failed \u2014 auth state mismatch, retry after delay',
|
|
2157
|
+
code: statusCode
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (statusCode === 429) {
|
|
2161
|
+
return {
|
|
2162
|
+
category: 'rate-limited',
|
|
2163
|
+
shouldReconnect: true,
|
|
2164
|
+
backoffMs: 3e5,
|
|
2165
|
+
// 5 minutes
|
|
2166
|
+
message: 'Rate limited by WhatsApp \u2014 cool-off period required',
|
|
2167
|
+
code: statusCode
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (statusCode === 503) {
|
|
2171
|
+
return {
|
|
2172
|
+
category: 'rate-limited',
|
|
2173
|
+
shouldReconnect: true,
|
|
2174
|
+
backoffMs: 6e4,
|
|
2175
|
+
// 1 minute
|
|
2176
|
+
message: 'WhatsApp service unavailable \u2014 temporary outage',
|
|
2177
|
+
code: statusCode
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (statusCode === 408) {
|
|
2181
|
+
return {
|
|
2182
|
+
category: 'recoverable',
|
|
2183
|
+
shouldReconnect: true,
|
|
2184
|
+
backoffMs: 5e3,
|
|
2185
|
+
// 5 seconds
|
|
2186
|
+
message: 'Connection timeout \u2014 network issue, safe to retry',
|
|
2187
|
+
code: statusCode
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
if (statusCode === 500) {
|
|
2191
|
+
return {
|
|
2192
|
+
category: 'recoverable',
|
|
2193
|
+
shouldReconnect: true,
|
|
2194
|
+
backoffMs: 1e4,
|
|
2195
|
+
// 10 seconds
|
|
2196
|
+
message: 'WhatsApp internal error \u2014 temporary server issue',
|
|
2197
|
+
code: statusCode
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (statusCode === 1e3) {
|
|
2201
|
+
return {
|
|
2202
|
+
category: 'recoverable',
|
|
2203
|
+
shouldReconnect: true,
|
|
2204
|
+
backoffMs: 2e3,
|
|
2205
|
+
// 2 seconds
|
|
2206
|
+
message: 'Connection closed gracefully \u2014 safe to reconnect',
|
|
2207
|
+
code: statusCode
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
category: 'unknown',
|
|
2212
|
+
shouldReconnect: true,
|
|
2213
|
+
backoffMs: 15e3,
|
|
2214
|
+
// 15 seconds
|
|
2215
|
+
message: `Unknown disconnect reason (code ${statusCode}) \u2014 reconnect with caution`,
|
|
2216
|
+
code: statusCode
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
var DEFAULT_HEALTH_CONFIG = {
|
|
2220
|
+
badMacThreshold: 3,
|
|
2221
|
+
badMacWindowMs: 6e4
|
|
2222
|
+
}
|
|
2223
|
+
var SessionHealthMonitor = class {
|
|
2224
|
+
config
|
|
2225
|
+
onDegraded
|
|
2226
|
+
onRecovered
|
|
2227
|
+
stats = {
|
|
2228
|
+
decryptSuccess: 0,
|
|
2229
|
+
decryptFail: 0,
|
|
2230
|
+
badMacCount: 0,
|
|
2231
|
+
isDegraded: false
|
|
2232
|
+
}
|
|
2233
|
+
badMacTimestamps = []
|
|
2234
|
+
constructor(config = {}) {
|
|
2235
|
+
this.config = { ...DEFAULT_HEALTH_CONFIG, ...config }
|
|
2236
|
+
this.onDegraded = config.onDegraded
|
|
2237
|
+
this.onRecovered = config.onRecovered
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Record successful decrypt
|
|
2241
|
+
*/
|
|
2242
|
+
recordDecryptSuccess() {
|
|
2243
|
+
this.stats.decryptSuccess++
|
|
2244
|
+
this.checkRecovery()
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Record failed decrypt (Bad MAC or similar)
|
|
2248
|
+
*/
|
|
2249
|
+
recordDecryptFail(isBadMac = false) {
|
|
2250
|
+
this.stats.decryptFail++
|
|
2251
|
+
if (isBadMac) {
|
|
2252
|
+
const now = Date.now()
|
|
2253
|
+
this.stats.badMacCount++
|
|
2254
|
+
this.stats.lastBadMac = new Date(now)
|
|
2255
|
+
this.badMacTimestamps.push(now)
|
|
2256
|
+
const cutoff = now - this.config.badMacWindowMs
|
|
2257
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff)
|
|
2258
|
+
if (!this.stats.isDegraded && this.badMacTimestamps.length >= this.config.badMacThreshold) {
|
|
2259
|
+
this.stats.isDegraded = true
|
|
2260
|
+
this.stats.degradedSince = new Date(now)
|
|
2261
|
+
this.onDegraded?.(this.getStats())
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Check if session has recovered from degraded state
|
|
2267
|
+
*/
|
|
2268
|
+
checkRecovery() {
|
|
2269
|
+
if (!this.stats.isDegraded) return
|
|
2270
|
+
const now = Date.now()
|
|
2271
|
+
const cutoff = now - this.config.badMacWindowMs
|
|
2272
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff)
|
|
2273
|
+
if (this.badMacTimestamps.length < this.config.badMacThreshold) {
|
|
2274
|
+
this.stats.isDegraded = false
|
|
2275
|
+
this.stats.degradedSince = void 0
|
|
2276
|
+
this.onRecovered?.(this.getStats())
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Get current health stats
|
|
2281
|
+
*/
|
|
2282
|
+
getStats() {
|
|
2283
|
+
return { ...this.stats }
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Reset all counters
|
|
2287
|
+
*/
|
|
2288
|
+
reset() {
|
|
2289
|
+
this.stats = {
|
|
2290
|
+
decryptSuccess: 0,
|
|
2291
|
+
decryptFail: 0,
|
|
2292
|
+
badMacCount: 0,
|
|
2293
|
+
isDegraded: false
|
|
2294
|
+
}
|
|
2295
|
+
this.badMacTimestamps = []
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
function wrapWithSessionStability(sock, config = {}) {
|
|
2299
|
+
const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver } = config
|
|
2300
|
+
const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null
|
|
2301
|
+
return new Proxy(sock, {
|
|
2302
|
+
get(target, prop) {
|
|
2303
|
+
if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
|
|
2304
|
+
return async (jid, content, options) => {
|
|
2305
|
+
const canonical = lidResolver.resolveCanonical(jid)
|
|
2306
|
+
return target.sendMessage(canonical, content, options)
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (prop === 'sessionHealthStats' && healthMonitor) {
|
|
2310
|
+
return healthMonitor.getStats()
|
|
2311
|
+
}
|
|
2312
|
+
if (prop === 'sessionHealthMonitor' && healthMonitor) {
|
|
2313
|
+
return healthMonitor
|
|
2314
|
+
}
|
|
2315
|
+
return target[prop]
|
|
2316
|
+
}
|
|
2317
|
+
})
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// presets.js
|
|
2321
|
+
var PRESETS = {
|
|
2322
|
+
conservative: {
|
|
2323
|
+
maxPerMinute: 5,
|
|
2324
|
+
maxPerHour: 100,
|
|
2325
|
+
maxPerDay: 800,
|
|
2326
|
+
minDelayMs: 2500,
|
|
2327
|
+
maxDelayMs: 7e3,
|
|
2328
|
+
newChatDelayMs: 4e3,
|
|
2329
|
+
warmupDays: 10,
|
|
2330
|
+
day1Limit: 15,
|
|
2331
|
+
growthFactor: 1.8,
|
|
2332
|
+
inactivityThresholdHours: 72,
|
|
2333
|
+
autoPauseAt: 'medium',
|
|
2334
|
+
groupMultiplier: 0.5,
|
|
2335
|
+
groupProfiles: false,
|
|
2336
|
+
logging: true
|
|
2337
|
+
},
|
|
2338
|
+
moderate: {
|
|
2339
|
+
maxPerMinute: 10,
|
|
2340
|
+
maxPerHour: 300,
|
|
2341
|
+
maxPerDay: 1500,
|
|
2342
|
+
minDelayMs: 1500,
|
|
2343
|
+
maxDelayMs: 5e3,
|
|
2344
|
+
newChatDelayMs: 3e3,
|
|
2345
|
+
warmupDays: 7,
|
|
2346
|
+
day1Limit: 20,
|
|
2347
|
+
growthFactor: 1.8,
|
|
2348
|
+
inactivityThresholdHours: 72,
|
|
2349
|
+
autoPauseAt: 'high',
|
|
2350
|
+
groupMultiplier: 0.7,
|
|
2351
|
+
groupProfiles: false,
|
|
2352
|
+
logging: true
|
|
2353
|
+
},
|
|
2354
|
+
aggressive: {
|
|
2355
|
+
maxPerMinute: 20,
|
|
2356
|
+
maxPerHour: 800,
|
|
2357
|
+
maxPerDay: 4e3,
|
|
2358
|
+
minDelayMs: 800,
|
|
2359
|
+
maxDelayMs: 3e3,
|
|
2360
|
+
newChatDelayMs: 2e3,
|
|
2361
|
+
warmupDays: 4,
|
|
2362
|
+
day1Limit: 35,
|
|
2363
|
+
growthFactor: 2,
|
|
2364
|
+
inactivityThresholdHours: 48,
|
|
2365
|
+
autoPauseAt: 'critical',
|
|
2366
|
+
groupMultiplier: 0.9,
|
|
2367
|
+
groupProfiles: false,
|
|
2368
|
+
logging: true
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function resolveConfig(input) {
|
|
2372
|
+
if (input === void 0) {
|
|
2373
|
+
return { ...PRESETS.conservative }
|
|
2374
|
+
}
|
|
2375
|
+
if (typeof input === 'string') {
|
|
2376
|
+
if (!(input in PRESETS)) {
|
|
2377
|
+
throw new Error(`Unknown preset "${input}". Valid: ${Object.keys(PRESETS).join(', ')}`)
|
|
2378
|
+
}
|
|
2379
|
+
return { ...PRESETS[input] }
|
|
2380
|
+
}
|
|
2381
|
+
const { preset = 'conservative', ...overrides } = input
|
|
2382
|
+
if (!(preset in PRESETS)) {
|
|
2383
|
+
throw new Error(`Unknown preset "${preset}". Valid: ${Object.keys(PRESETS).join(', ')}`)
|
|
2384
|
+
}
|
|
2385
|
+
return { ...PRESETS[preset], ...overrides }
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// persist.js
|
|
2389
|
+
var fs = __toESM(require('fs'), 1)
|
|
2390
|
+
var KNOWN_CHATS_MAX = 1e3
|
|
2391
|
+
var DEBOUNCE_MS = 5e3
|
|
2392
|
+
var StateManager = class {
|
|
2393
|
+
path
|
|
2394
|
+
debounceTimer = null
|
|
2395
|
+
constructor(filePath) {
|
|
2396
|
+
this.path = filePath
|
|
2397
|
+
}
|
|
2398
|
+
load() {
|
|
2399
|
+
try {
|
|
2400
|
+
const raw = fs.readFileSync(this.path, 'utf-8')
|
|
2401
|
+
const parsed = JSON.parse(raw)
|
|
2402
|
+
if (parsed.version !== 3) {
|
|
2403
|
+
process.stderr.write('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh\n')
|
|
2404
|
+
return null
|
|
2405
|
+
}
|
|
2406
|
+
return parsed
|
|
2407
|
+
} catch {
|
|
2408
|
+
if (fs.existsSync(this.path)) {
|
|
2409
|
+
process.stderr.write('[baileys-antiban] WARN: corrupt state file, starting fresh\n')
|
|
2410
|
+
}
|
|
2411
|
+
return null
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
/** Debounced save — called after every send (5s delay) */
|
|
2415
|
+
saveDebounced(state) {
|
|
2416
|
+
if (this.debounceTimer) {
|
|
2417
|
+
clearTimeout(this.debounceTimer)
|
|
2418
|
+
}
|
|
2419
|
+
this.debounceTimer = setTimeout(() => {
|
|
2420
|
+
this.writeFile(state)
|
|
2421
|
+
this.debounceTimer = null
|
|
2422
|
+
}, DEBOUNCE_MS)
|
|
2423
|
+
}
|
|
2424
|
+
/** Immediate save — called after health events (ban/restriction) */
|
|
2425
|
+
saveImmediate(state) {
|
|
2426
|
+
if (this.debounceTimer) {
|
|
2427
|
+
clearTimeout(this.debounceTimer)
|
|
2428
|
+
this.debounceTimer = null
|
|
2429
|
+
}
|
|
2430
|
+
this.writeFile(state)
|
|
2431
|
+
}
|
|
2432
|
+
/** Flush/cancel pending debounced write (for tests and process exit) */
|
|
2433
|
+
flush() {
|
|
2434
|
+
if (this.debounceTimer) {
|
|
2435
|
+
clearTimeout(this.debounceTimer)
|
|
2436
|
+
this.debounceTimer = null
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
destroy() {
|
|
2440
|
+
this.flush()
|
|
2441
|
+
}
|
|
2442
|
+
writeFile(state) {
|
|
2443
|
+
const toSave = {
|
|
2444
|
+
...state,
|
|
2445
|
+
savedAt: Date.now(),
|
|
2446
|
+
// LRU eviction: keep last KNOWN_CHATS_MAX entries
|
|
2447
|
+
knownChats:
|
|
2448
|
+
state.knownChats.length > KNOWN_CHATS_MAX ? state.knownChats.slice(-KNOWN_CHATS_MAX) : state.knownChats
|
|
2449
|
+
}
|
|
2450
|
+
try {
|
|
2451
|
+
fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8')
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
process.stderr.write(`[baileys-antiban] WARN: failed to write state to ${this.path}: ${err}\n`)
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// profiles.js
|
|
2459
|
+
function isGroup(jid) {
|
|
2460
|
+
return jid.endsWith('@g.us')
|
|
2461
|
+
}
|
|
2462
|
+
function isNewsletter(jid) {
|
|
2463
|
+
return jid.endsWith('@newsletter')
|
|
2464
|
+
}
|
|
2465
|
+
function isBroadcast(jid) {
|
|
2466
|
+
return jid === 'status@broadcast' || jid.endsWith('@broadcast')
|
|
2467
|
+
}
|
|
2468
|
+
function shouldUseGroupProfile(jid) {
|
|
2469
|
+
return isGroup(jid) || isNewsletter(jid)
|
|
2470
|
+
}
|
|
2471
|
+
function applyGroupMultiplier(limits, multiplier) {
|
|
2472
|
+
return {
|
|
2473
|
+
maxPerMinute: Math.max(1, Math.floor(limits.maxPerMinute * multiplier)),
|
|
2474
|
+
maxPerHour: Math.max(1, Math.floor(limits.maxPerHour * multiplier)),
|
|
2475
|
+
maxPerDay: Math.max(1, Math.floor(limits.maxPerDay * multiplier))
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// antiban.js
|
|
2480
|
+
function isLegacyConfig(cfg) {
|
|
2481
|
+
if (typeof cfg !== 'object' || cfg === null) return false
|
|
2482
|
+
return (
|
|
2483
|
+
'rateLimiter' in cfg ||
|
|
2484
|
+
'warmUp' in cfg ||
|
|
2485
|
+
'health' in cfg ||
|
|
2486
|
+
'timelock' in cfg ||
|
|
2487
|
+
'replyRatio' in cfg ||
|
|
2488
|
+
'contactGraph' in cfg ||
|
|
2489
|
+
'presence' in cfg ||
|
|
2490
|
+
'retryTracker' in cfg ||
|
|
2491
|
+
'reconnectThrottle' in cfg ||
|
|
2492
|
+
'lidResolver' in cfg ||
|
|
2493
|
+
'jidCanonicalizer' in cfg ||
|
|
2494
|
+
'sessionStability' in cfg
|
|
2495
|
+
)
|
|
2496
|
+
}
|
|
2497
|
+
function mapLegacyToFlat(legacy) {
|
|
2498
|
+
process.stderr.write('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. Migrate to flat config: new AntiBan({ maxPerMinute: 8 }).\n')
|
|
2499
|
+
const flat = {}
|
|
2500
|
+
if (legacy.rateLimiter?.maxPerMinute !== void 0) flat.maxPerMinute = legacy.rateLimiter.maxPerMinute
|
|
2501
|
+
if (legacy.rateLimiter?.maxPerHour !== void 0) flat.maxPerHour = legacy.rateLimiter.maxPerHour
|
|
2502
|
+
if (legacy.rateLimiter?.maxPerDay !== void 0) flat.maxPerDay = legacy.rateLimiter.maxPerDay
|
|
2503
|
+
if (legacy.rateLimiter?.minDelayMs !== void 0) flat.minDelayMs = legacy.rateLimiter.minDelayMs
|
|
2504
|
+
if (legacy.rateLimiter?.maxDelayMs !== void 0) flat.maxDelayMs = legacy.rateLimiter.maxDelayMs
|
|
2505
|
+
if (legacy.rateLimiter?.newChatDelayMs !== void 0) flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs
|
|
2506
|
+
if (legacy.warmUp?.warmUpDays !== void 0) flat.warmupDays = legacy.warmUp.warmUpDays
|
|
2507
|
+
if (legacy.warmUp?.day1Limit !== void 0) flat.day1Limit = legacy.warmUp.day1Limit
|
|
2508
|
+
if (legacy.warmUp?.growthFactor !== void 0) flat.growthFactor = legacy.warmUp.growthFactor
|
|
2509
|
+
if (legacy.logging !== void 0) flat.logging = legacy.logging
|
|
2510
|
+
return flat
|
|
2511
|
+
}
|
|
2512
|
+
var AntiBan = class {
|
|
2513
|
+
rateLimiter
|
|
2514
|
+
warmUp
|
|
2515
|
+
health
|
|
2516
|
+
timelockGuard
|
|
2517
|
+
replyRatioGuard
|
|
2518
|
+
contactGraphWarmer
|
|
2519
|
+
presenceChoreographer
|
|
2520
|
+
retryTrackerModule
|
|
2521
|
+
reconnectThrottleModule
|
|
2522
|
+
lidResolverModule = null
|
|
2523
|
+
jidCanonicalizerModule = null
|
|
2524
|
+
sessionStabilityMonitor = null
|
|
2525
|
+
stateManager = null
|
|
2526
|
+
resolvedConfig
|
|
2527
|
+
logging
|
|
2528
|
+
stats = {
|
|
2529
|
+
messagesAllowed: 0,
|
|
2530
|
+
messagesBlocked: 0,
|
|
2531
|
+
totalDelayMs: 0
|
|
2532
|
+
}
|
|
2533
|
+
constructor(input, warmUpStateArg) {
|
|
2534
|
+
let flatConfig
|
|
2535
|
+
let legacyPassthrough = null
|
|
2536
|
+
let warmUpState = warmUpStateArg
|
|
2537
|
+
if (isLegacyConfig(input)) {
|
|
2538
|
+
legacyPassthrough = input
|
|
2539
|
+
flatConfig = mapLegacyToFlat(legacyPassthrough)
|
|
2540
|
+
} else {
|
|
2541
|
+
flatConfig = {}
|
|
2542
|
+
legacyPassthrough = null
|
|
2543
|
+
}
|
|
2544
|
+
const cfg = isLegacyConfig(input) ? resolveConfig(flatConfig) : resolveConfig(input)
|
|
2545
|
+
this.resolvedConfig = cfg
|
|
2546
|
+
let savedState = null
|
|
2547
|
+
if (cfg.persist) {
|
|
2548
|
+
this.stateManager = new StateManager(cfg.persist)
|
|
2549
|
+
savedState = this.stateManager.load()
|
|
2550
|
+
if (savedState) {
|
|
2551
|
+
warmUpState = savedState.warmup
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
this.logging = cfg.logging ?? true
|
|
2555
|
+
this._log = (msg) => { if (this.logging) process.stdout.write(`[antiban] ${msg}\n`) }
|
|
2556
|
+
this.rateLimiter = new RateLimiter({
|
|
2557
|
+
maxPerMinute: cfg.maxPerMinute,
|
|
2558
|
+
maxPerHour: cfg.maxPerHour,
|
|
2559
|
+
maxPerDay: cfg.maxPerDay,
|
|
2560
|
+
minDelayMs: cfg.minDelayMs,
|
|
2561
|
+
maxDelayMs: cfg.maxDelayMs,
|
|
2562
|
+
newChatDelayMs: cfg.newChatDelayMs,
|
|
2563
|
+
...(legacyPassthrough?.rateLimiter || {})
|
|
2564
|
+
})
|
|
2565
|
+
if (savedState?.knownChats) {
|
|
2566
|
+
this.rateLimiter.restoreKnownChats(savedState.knownChats)
|
|
2567
|
+
}
|
|
2568
|
+
this.warmUp = new WarmUp(
|
|
2569
|
+
{
|
|
2570
|
+
warmUpDays: cfg.warmupDays,
|
|
2571
|
+
day1Limit: cfg.day1Limit,
|
|
2572
|
+
growthFactor: cfg.growthFactor,
|
|
2573
|
+
inactivityThresholdHours: cfg.inactivityThresholdHours,
|
|
2574
|
+
...(legacyPassthrough?.warmUp || {})
|
|
2575
|
+
},
|
|
2576
|
+
warmUpState
|
|
2577
|
+
)
|
|
2578
|
+
this.health = new HealthMonitor({
|
|
2579
|
+
autoPauseAt: cfg.autoPauseAt,
|
|
2580
|
+
...(legacyPassthrough?.health || {}),
|
|
2581
|
+
onRiskChange: status => {
|
|
2582
|
+
const emoji = { low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' }
|
|
2583
|
+
this._log(`${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`)
|
|
2584
|
+
this._log(status.recommendation)
|
|
2585
|
+
status.reasons.forEach(r => this._log(` \u2192 ${r}`))
|
|
2586
|
+
legacyPassthrough?.health?.onRiskChange?.(status)
|
|
2587
|
+
}
|
|
2588
|
+
})
|
|
2589
|
+
this.timelockGuard = new TimelockGuard({
|
|
2590
|
+
...(legacyPassthrough?.timelock || {}),
|
|
2591
|
+
onTimelockDetected: state => {
|
|
2592
|
+
this.health.recordReachoutTimelock(state.enforcementType)
|
|
2593
|
+
this._log(`REACHOUT TIMELOCKED \u2014 ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`)
|
|
2594
|
+
legacyPassthrough?.timelock?.onTimelockDetected?.(state)
|
|
2595
|
+
},
|
|
2596
|
+
onTimelockLifted: state => {
|
|
2597
|
+
this._log('Timelock lifted \u2014 resuming new contact messages')
|
|
2598
|
+
legacyPassthrough?.timelock?.onTimelockLifted?.(state)
|
|
2599
|
+
}
|
|
2600
|
+
})
|
|
2601
|
+
this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio)
|
|
2602
|
+
this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph)
|
|
2603
|
+
this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence)
|
|
2604
|
+
this.retryTrackerModule = new RetryReasonTracker({
|
|
2605
|
+
...(legacyPassthrough?.retryTracker || {}),
|
|
2606
|
+
onSpiral: (msgId, reason) => {
|
|
2607
|
+
this._log(`\u26A0\uFE0F Message ${msgId} stuck in retry spiral (${reason})`)
|
|
2608
|
+
legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason)
|
|
2609
|
+
}
|
|
2610
|
+
})
|
|
2611
|
+
this.reconnectThrottleModule = new PostReconnectThrottle({
|
|
2612
|
+
...(legacyPassthrough?.reconnectThrottle || {}),
|
|
2613
|
+
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute
|
|
2614
|
+
})
|
|
2615
|
+
if (legacyPassthrough?.jidCanonicalizer?.enabled) {
|
|
2616
|
+
if (legacyPassthrough.jidCanonicalizer.resolver) {
|
|
2617
|
+
this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer)
|
|
2618
|
+
this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver
|
|
2619
|
+
} else {
|
|
2620
|
+
const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig
|
|
2621
|
+
const resolver = new LidResolver(resolverConfig)
|
|
2622
|
+
this.lidResolverModule = resolver
|
|
2623
|
+
this.jidCanonicalizerModule = new JidCanonicalizer({
|
|
2624
|
+
...legacyPassthrough.jidCanonicalizer,
|
|
2625
|
+
resolver
|
|
2626
|
+
})
|
|
2627
|
+
}
|
|
2628
|
+
} else if (legacyPassthrough?.lidResolver) {
|
|
2629
|
+
this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver)
|
|
2630
|
+
}
|
|
2631
|
+
if (legacyPassthrough?.sessionStability?.enabled) {
|
|
2632
|
+
const healthConfig = {
|
|
2633
|
+
badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
|
|
2634
|
+
badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
|
|
2635
|
+
onDegraded: stats => {
|
|
2636
|
+
this._log(`\u{1F534} SESSION DEGRADED \u2014 Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 6e4}ms`)
|
|
2637
|
+
this._log('Consider restarting session or switching to LID-based canonical form')
|
|
2638
|
+
},
|
|
2639
|
+
onRecovered: () => {
|
|
2640
|
+
this._log('\u{1F7E2} SESSION RECOVERED \u2014 decrypt success rate improved')
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig)
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Check if a message can be sent and get required delay.
|
|
2648
|
+
* Call this BEFORE every sendMessage().
|
|
2649
|
+
*/
|
|
2650
|
+
async beforeSend(recipient, content) {
|
|
2651
|
+
const healthStatus = this.health.getStatus()
|
|
2652
|
+
if (this.health.isPaused()) {
|
|
2653
|
+
this.stats.messagesBlocked++
|
|
2654
|
+
this._log(`\u26D4 BLOCKED \u2014 health risk too high (${healthStatus.risk})`)
|
|
2655
|
+
return {
|
|
2656
|
+
allowed: false,
|
|
2657
|
+
delayMs: 0,
|
|
2658
|
+
reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
|
|
2659
|
+
health: healthStatus
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const timelockDecision = this.timelockGuard.canSend(recipient)
|
|
2663
|
+
if (!timelockDecision.allowed) {
|
|
2664
|
+
this.stats.messagesBlocked++
|
|
2665
|
+
this._log(`TIMELOCKED \u2014 ${timelockDecision.reason}`)
|
|
2666
|
+
return {
|
|
2667
|
+
allowed: false,
|
|
2668
|
+
delayMs: 0,
|
|
2669
|
+
reason: timelockDecision.reason,
|
|
2670
|
+
health: healthStatus
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (!this.warmUp.canSend()) {
|
|
2674
|
+
this.stats.messagesBlocked++
|
|
2675
|
+
const warmUpStatus = this.warmUp.getStatus()
|
|
2676
|
+
this._log(`\u23F3 BLOCKED \u2014 warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`)
|
|
2677
|
+
return {
|
|
2678
|
+
allowed: false,
|
|
2679
|
+
delayMs: 0,
|
|
2680
|
+
reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
|
|
2681
|
+
health: healthStatus,
|
|
2682
|
+
warmUpDay: warmUpStatus.day
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient)
|
|
2686
|
+
if (!contactGraphDecision.allowed) {
|
|
2687
|
+
this.stats.messagesBlocked++
|
|
2688
|
+
this._log(`\u{1F4CA} BLOCKED \u2014 contact graph: ${contactGraphDecision.reason}`)
|
|
2689
|
+
return {
|
|
2690
|
+
allowed: false,
|
|
2691
|
+
delayMs: 0,
|
|
2692
|
+
reason: `Contact graph: ${contactGraphDecision.reason}`,
|
|
2693
|
+
health: healthStatus
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient)
|
|
2697
|
+
if (!replyRatioDecision.allowed) {
|
|
2698
|
+
this.stats.messagesBlocked++
|
|
2699
|
+
this._log(`\u{1F4AC} BLOCKED \u2014 reply ratio: ${replyRatioDecision.reason}`)
|
|
2700
|
+
return {
|
|
2701
|
+
allowed: false,
|
|
2702
|
+
delayMs: 0,
|
|
2703
|
+
reason: `Reply ratio: ${replyRatioDecision.reason}`,
|
|
2704
|
+
health: healthStatus
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend()
|
|
2708
|
+
if (!reconnectThrottleDecision.allowed) {
|
|
2709
|
+
this.stats.messagesBlocked++
|
|
2710
|
+
this._log(`\u{1F504} BLOCKED \u2014 reconnect throttle: ${reconnectThrottleDecision.reason}`)
|
|
2711
|
+
return {
|
|
2712
|
+
allowed: false,
|
|
2713
|
+
delayMs: reconnectThrottleDecision.retryAfterMs || 0,
|
|
2714
|
+
reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
|
|
2715
|
+
health: healthStatus
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
|
|
2719
|
+
const groupLimits = applyGroupMultiplier(
|
|
2720
|
+
{
|
|
2721
|
+
maxPerMinute: this.resolvedConfig.maxPerMinute,
|
|
2722
|
+
maxPerHour: this.resolvedConfig.maxPerHour,
|
|
2723
|
+
maxPerDay: this.resolvedConfig.maxPerDay
|
|
2724
|
+
},
|
|
2725
|
+
this.resolvedConfig.groupMultiplier
|
|
2726
|
+
)
|
|
2727
|
+
const stats = this.rateLimiter.getStats()
|
|
2728
|
+
if (
|
|
2729
|
+
stats.lastMinute >= groupLimits.maxPerMinute ||
|
|
2730
|
+
stats.lastHour >= groupLimits.maxPerHour ||
|
|
2731
|
+
stats.lastDay >= groupLimits.maxPerDay
|
|
2732
|
+
) {
|
|
2733
|
+
this.stats.messagesBlocked++
|
|
2734
|
+
this._log(`\u{1F6AB} BLOCKED \u2014 group rate limit exceeded for ${recipient}`)
|
|
2735
|
+
return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus }
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
let delay = await this.rateLimiter.getDelay(recipient, content)
|
|
2739
|
+
if (delay === -1) {
|
|
2740
|
+
this.stats.messagesBlocked++
|
|
2741
|
+
this._log(`\u{1F6AB} BLOCKED \u2014 rate limit or identical message spam`)
|
|
2742
|
+
return {
|
|
2743
|
+
allowed: false,
|
|
2744
|
+
delayMs: 0,
|
|
2745
|
+
reason: 'Rate limit exceeded or identical message spam detected',
|
|
2746
|
+
health: healthStatus
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
const activityFactor = this.presenceChoreographer.getCurrentActivityFactor()
|
|
2750
|
+
if (activityFactor < 1) {
|
|
2751
|
+
const multiplier = Math.min(5, 1 / activityFactor)
|
|
2752
|
+
delay = Math.floor(delay * multiplier)
|
|
2753
|
+
}
|
|
2754
|
+
const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction()
|
|
2755
|
+
if (distractionCheck.pause) {
|
|
2756
|
+
delay += distractionCheck.durationMs
|
|
2757
|
+
this._log(`\u23F8\uFE0F Distraction pause: +${Math.floor(distractionCheck.durationMs / 6e4)}min`)
|
|
2758
|
+
}
|
|
2759
|
+
const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap()
|
|
2760
|
+
if (offlineCheck.offline) {
|
|
2761
|
+
delay += offlineCheck.durationMs
|
|
2762
|
+
this._log(`\u{1F4F4} Offline gap: +${Math.floor(offlineCheck.durationMs / 6e4)}min`)
|
|
2763
|
+
}
|
|
2764
|
+
this.stats.totalDelayMs += delay
|
|
2765
|
+
return {
|
|
2766
|
+
allowed: true,
|
|
2767
|
+
delayMs: delay,
|
|
2768
|
+
health: healthStatus
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Record a successfully sent message.
|
|
2773
|
+
* Call this AFTER every successful sendMessage().
|
|
2774
|
+
*/
|
|
2775
|
+
afterSend(recipient, content) {
|
|
2776
|
+
this.rateLimiter.record(recipient, content)
|
|
2777
|
+
this.warmUp.record()
|
|
2778
|
+
this.replyRatioGuard.recordSent(recipient)
|
|
2779
|
+
this.stats.messagesAllowed++
|
|
2780
|
+
this.persistStateDebounced()
|
|
2781
|
+
}
|
|
2782
|
+
/**
|
|
2783
|
+
* Record a failed message send
|
|
2784
|
+
*/
|
|
2785
|
+
afterSendFailed(error) {
|
|
2786
|
+
this.health.recordMessageFailed(error)
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Record a disconnection (call from connection.update handler)
|
|
2790
|
+
*/
|
|
2791
|
+
onDisconnect(reason) {
|
|
2792
|
+
this.health.recordDisconnect(reason)
|
|
2793
|
+
this.reconnectThrottleModule.onDisconnect()
|
|
2794
|
+
const reasonStr = String(reason)
|
|
2795
|
+
if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
|
|
2796
|
+
this.persistStateImmediate()
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Record a successful reconnection
|
|
2801
|
+
*/
|
|
2802
|
+
onReconnect() {
|
|
2803
|
+
this.health.recordReconnect()
|
|
2804
|
+
this.reconnectThrottleModule.onReconnect()
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Handle incoming message — record in reply ratio + contact graph.
|
|
2808
|
+
* Returns suggested reply if reply ratio suggests auto-reply.
|
|
2809
|
+
*/
|
|
2810
|
+
onIncomingMessage(jid, msgText) {
|
|
2811
|
+
this.replyRatioGuard.recordReceived(jid)
|
|
2812
|
+
this.contactGraphWarmer.onIncomingMessage(jid)
|
|
2813
|
+
return this.replyRatioGuard.suggestReply(jid, msgText)
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* Get comprehensive stats
|
|
2817
|
+
*/
|
|
2818
|
+
getStats() {
|
|
2819
|
+
const stats = {
|
|
2820
|
+
...this.stats,
|
|
2821
|
+
health: this.health.getStatus(),
|
|
2822
|
+
warmUp: this.warmUp.getStatus(),
|
|
2823
|
+
rateLimiter: this.rateLimiter.getStats()
|
|
2824
|
+
}
|
|
2825
|
+
if (this.replyRatioGuard['config']?.enabled) {
|
|
2826
|
+
stats.replyRatio = this.replyRatioGuard.getStats()
|
|
2827
|
+
}
|
|
2828
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
2829
|
+
stats.contactGraph = this.contactGraphWarmer.getStats()
|
|
2830
|
+
}
|
|
2831
|
+
if (this.presenceChoreographer['config']?.enabled) {
|
|
2832
|
+
stats.presence = this.presenceChoreographer.getStats()
|
|
2833
|
+
}
|
|
2834
|
+
if (this.retryTrackerModule['config']?.enabled) {
|
|
2835
|
+
stats.retryTracker = this.retryTrackerModule.getStats()
|
|
2836
|
+
}
|
|
2837
|
+
if (this.reconnectThrottleModule['config']?.enabled) {
|
|
2838
|
+
stats.reconnectThrottle = this.reconnectThrottleModule.getStats()
|
|
2839
|
+
}
|
|
2840
|
+
if (this.lidResolverModule) {
|
|
2841
|
+
stats.lidResolver = this.lidResolverModule.getStats()
|
|
2842
|
+
}
|
|
2843
|
+
if (this.jidCanonicalizerModule) {
|
|
2844
|
+
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats()
|
|
2845
|
+
}
|
|
2846
|
+
if (this.sessionStabilityMonitor) {
|
|
2847
|
+
stats.sessionStability = this.sessionStabilityMonitor.getStats()
|
|
2848
|
+
}
|
|
2849
|
+
return stats
|
|
2850
|
+
}
|
|
2851
|
+
/** Get the timelock guard for direct access */
|
|
2852
|
+
get timelock() {
|
|
2853
|
+
return this.timelockGuard
|
|
2854
|
+
}
|
|
2855
|
+
/** Get the reply ratio guard for direct access */
|
|
2856
|
+
get replyRatio() {
|
|
2857
|
+
return this.replyRatioGuard
|
|
2858
|
+
}
|
|
2859
|
+
/** Get the contact graph warmer for direct access */
|
|
2860
|
+
get contactGraph() {
|
|
2861
|
+
return this.contactGraphWarmer
|
|
2862
|
+
}
|
|
2863
|
+
/** Get the presence choreographer for direct access */
|
|
2864
|
+
get presence() {
|
|
2865
|
+
return this.presenceChoreographer
|
|
2866
|
+
}
|
|
2867
|
+
/** Get the retry tracker for direct access */
|
|
2868
|
+
get retryTracker() {
|
|
2869
|
+
return this.retryTrackerModule
|
|
2870
|
+
}
|
|
2871
|
+
/** Get the reconnect throttle for direct access */
|
|
2872
|
+
get reconnectThrottle() {
|
|
2873
|
+
return this.reconnectThrottleModule
|
|
2874
|
+
}
|
|
2875
|
+
/** Get the LID resolver for direct access */
|
|
2876
|
+
get lidResolver() {
|
|
2877
|
+
return this.lidResolverModule
|
|
2878
|
+
}
|
|
2879
|
+
/** Get the JID canonicalizer for direct access */
|
|
2880
|
+
get jidCanonicalizer() {
|
|
2881
|
+
return this.jidCanonicalizerModule
|
|
2882
|
+
}
|
|
2883
|
+
/** Get the session stability monitor for direct access */
|
|
2884
|
+
get sessionStability() {
|
|
2885
|
+
return this.sessionStabilityMonitor
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Export warm-up state for persistence between restarts
|
|
2889
|
+
*/
|
|
2890
|
+
exportWarmUpState() {
|
|
2891
|
+
return this.warmUp.exportState()
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Force pause all sending
|
|
2895
|
+
*/
|
|
2896
|
+
pause() {
|
|
2897
|
+
this.health.setPaused(true)
|
|
2898
|
+
this._log('\u23F8\uFE0F Sending paused manually')
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Resume sending
|
|
2902
|
+
*/
|
|
2903
|
+
resume() {
|
|
2904
|
+
this.health.setPaused(false)
|
|
2905
|
+
this._log('\u25B6\uFE0F Sending resumed')
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Reset everything (use after a ban period)
|
|
2909
|
+
*/
|
|
2910
|
+
reset() {
|
|
2911
|
+
this.timelockGuard.reset()
|
|
2912
|
+
this.health.reset()
|
|
2913
|
+
this.warmUp.reset()
|
|
2914
|
+
this.replyRatioGuard.reset()
|
|
2915
|
+
this.contactGraphWarmer.reset()
|
|
2916
|
+
this.presenceChoreographer.reset()
|
|
2917
|
+
this.retryTrackerModule.destroy()
|
|
2918
|
+
this.reconnectThrottleModule.destroy()
|
|
2919
|
+
this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 }
|
|
2920
|
+
this._log('\u{1F504} Reset \u2014 starting fresh warm-up')
|
|
2921
|
+
}
|
|
2922
|
+
persistStateDebounced() {
|
|
2923
|
+
if (!this.stateManager) return
|
|
2924
|
+
const state = {
|
|
2925
|
+
warmup: this.warmUp.exportState(),
|
|
2926
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
2927
|
+
savedAt: Date.now(),
|
|
2928
|
+
version: 3
|
|
2929
|
+
}
|
|
2930
|
+
this.stateManager.saveDebounced(state)
|
|
2931
|
+
}
|
|
2932
|
+
persistStateImmediate() {
|
|
2933
|
+
if (!this.stateManager) return
|
|
2934
|
+
const state = {
|
|
2935
|
+
warmup: this.warmUp.exportState(),
|
|
2936
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
2937
|
+
savedAt: Date.now(),
|
|
2938
|
+
version: 3
|
|
2939
|
+
}
|
|
2940
|
+
this.stateManager.saveImmediate(state)
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Clean up all timers and resources.
|
|
2944
|
+
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
2945
|
+
*/
|
|
2946
|
+
destroy() {
|
|
2947
|
+
this.stateManager?.destroy()
|
|
2948
|
+
this.timelockGuard.reset()
|
|
2949
|
+
this.replyRatioGuard.reset()
|
|
2950
|
+
this.contactGraphWarmer.reset()
|
|
2951
|
+
this.presenceChoreographer.reset()
|
|
2952
|
+
this.retryTrackerModule.destroy()
|
|
2953
|
+
this.reconnectThrottleModule.destroy()
|
|
2954
|
+
this.jidCanonicalizerModule?.destroy()
|
|
2955
|
+
this.lidResolverModule?.destroy()
|
|
2956
|
+
this.sessionStabilityMonitor?.reset()
|
|
2957
|
+
this._log('\u{1F9F9} Destroyed \u2014 all timers cleared')
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// lidFirstResolver.js
|
|
2962
|
+
var fs2 = __toESM(require('fs'), 1)
|
|
2963
|
+
var path = __toESM(require('path'), 1)
|
|
2964
|
+
var LidFirstResolver = class {
|
|
2965
|
+
lidToPhone = /* @__PURE__ */ new Map()
|
|
2966
|
+
phoneToLid = /* @__PURE__ */ new Map()
|
|
2967
|
+
// phone → lid (quick reverse lookup)
|
|
2968
|
+
/**
|
|
2969
|
+
* Load mappings from Baileys auth state directory.
|
|
2970
|
+
* Looks for lid-mapping-*_reverse.json files.
|
|
2971
|
+
*/
|
|
2972
|
+
loadFromAuthDir(authDir) {
|
|
2973
|
+
try {
|
|
2974
|
+
if (!fs2.existsSync(authDir)) {
|
|
2975
|
+
return
|
|
2976
|
+
}
|
|
2977
|
+
const files = fs2.readdirSync(authDir)
|
|
2978
|
+
const reverseMappingFiles = files.filter(f => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'))
|
|
2979
|
+
for (const file of reverseMappingFiles) {
|
|
2980
|
+
const filePath = path.join(authDir, file)
|
|
2981
|
+
const content = fs2.readFileSync(filePath, 'utf-8')
|
|
2982
|
+
const data = JSON.parse(content)
|
|
2983
|
+
for (const [lid, pnJid] of Object.entries(data)) {
|
|
2984
|
+
if (typeof pnJid === 'string') {
|
|
2985
|
+
const phone = this.extractPhone(pnJid)
|
|
2986
|
+
if (phone && lid.endsWith('@lid')) {
|
|
2987
|
+
const mapping = {
|
|
2988
|
+
lid: this.normalizeLid(lid),
|
|
2989
|
+
phone,
|
|
2990
|
+
learnedAt: Date.now(),
|
|
2991
|
+
source: 'auth-dir'
|
|
2992
|
+
}
|
|
2993
|
+
this.lidToPhone.set(mapping.lid, mapping)
|
|
2994
|
+
this.phoneToLid.set(phone, mapping.lid)
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
} catch (error) {}
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Learn a new mapping from a Baileys event (messages, contacts, etc.).
|
|
3003
|
+
* Accepts partial data — will extract what it can.
|
|
3004
|
+
*/
|
|
3005
|
+
learnFromEvent(event) {
|
|
3006
|
+
try {
|
|
3007
|
+
if (event.key?.remoteJid) {
|
|
3008
|
+
const jid = event.key.remoteJid
|
|
3009
|
+
this.learnJid(jid, 'event')
|
|
3010
|
+
}
|
|
3011
|
+
if (event.key?.participant) {
|
|
3012
|
+
const jid = event.key.participant
|
|
3013
|
+
this.learnJid(jid, 'event')
|
|
3014
|
+
}
|
|
3015
|
+
if (event.id) {
|
|
3016
|
+
this.learnJid(event.id, 'event')
|
|
3017
|
+
}
|
|
3018
|
+
if (event.pushName && event.key?.remoteJid) {
|
|
3019
|
+
this.learnJid(event.key.remoteJid, 'event')
|
|
3020
|
+
}
|
|
3021
|
+
} catch (error) {}
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Resolve phone number or phone JID to LID JID.
|
|
3025
|
+
* Returns null if not known.
|
|
3026
|
+
*/
|
|
3027
|
+
resolveToLID(phoneOrJid) {
|
|
3028
|
+
const phone = this.extractPhone(phoneOrJid)
|
|
3029
|
+
if (!phone) return null
|
|
3030
|
+
return this.phoneToLid.get(phone) || null
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Resolve LID JID to phone number.
|
|
3034
|
+
* Returns null if not known.
|
|
3035
|
+
*/
|
|
3036
|
+
resolveToPhone(lid) {
|
|
3037
|
+
const normalized = this.normalizeLid(lid)
|
|
3038
|
+
const mapping = this.lidToPhone.get(normalized)
|
|
3039
|
+
return mapping ? mapping.phone : null
|
|
3040
|
+
}
|
|
3041
|
+
/**
|
|
3042
|
+
* Get full mapping for a given JID (either LID or phone).
|
|
3043
|
+
* Returns null if not known.
|
|
3044
|
+
*/
|
|
3045
|
+
getMapping(jid) {
|
|
3046
|
+
const normalized = this.normalizeLid(jid)
|
|
3047
|
+
const byLid = this.lidToPhone.get(normalized)
|
|
3048
|
+
if (byLid) return byLid
|
|
3049
|
+
const phone = this.extractPhone(jid)
|
|
3050
|
+
if (phone) {
|
|
3051
|
+
const lid = this.phoneToLid.get(phone)
|
|
3052
|
+
if (lid) return this.lidToPhone.get(lid) || null
|
|
3053
|
+
}
|
|
3054
|
+
return null
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Get total number of known mappings.
|
|
3058
|
+
*/
|
|
3059
|
+
size() {
|
|
3060
|
+
return this.lidToPhone.size
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Clear all mappings.
|
|
3064
|
+
*/
|
|
3065
|
+
clear() {
|
|
3066
|
+
this.lidToPhone.clear()
|
|
3067
|
+
this.phoneToLid.clear()
|
|
3068
|
+
}
|
|
3069
|
+
// Private helpers
|
|
3070
|
+
learnJid(_jid, _source) {}
|
|
3071
|
+
extractPhone(jid) {
|
|
3072
|
+
if (!jid) return null
|
|
3073
|
+
let cleaned = jid.replace('@s.whatsapp.net', '')
|
|
3074
|
+
cleaned = cleaned.replace(/:\d+$/, '')
|
|
3075
|
+
if (/^\d+$/.test(cleaned)) {
|
|
3076
|
+
return cleaned
|
|
3077
|
+
}
|
|
3078
|
+
return null
|
|
3079
|
+
}
|
|
3080
|
+
normalizeLid(lid) {
|
|
3081
|
+
return lid.replace(/:\d+@/, '@')
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function createLidFirstResolver() {
|
|
3085
|
+
return new LidFirstResolver()
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
// retryReason.js
|
|
3089
|
+
var MessageRetryReason
|
|
3090
|
+
;(function (MessageRetryReason2) {
|
|
3091
|
+
MessageRetryReason2[(MessageRetryReason2['UnknownError'] = 0)] = 'UnknownError'
|
|
3092
|
+
MessageRetryReason2[(MessageRetryReason2['GenericError'] = 1)] = 'GenericError'
|
|
3093
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidKeyId'] = 3)] = 'SignalErrorInvalidKeyId'
|
|
3094
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidMessage'] = 4)] = 'SignalErrorInvalidMessage'
|
|
3095
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorNoSession'] = 5)] = 'SignalErrorNoSession'
|
|
3096
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorBadMac'] = 7)] = 'SignalErrorBadMac'
|
|
3097
|
+
MessageRetryReason2[(MessageRetryReason2['MessageExpired'] = 8)] = 'MessageExpired'
|
|
3098
|
+
MessageRetryReason2[(MessageRetryReason2['DecryptionError'] = 9)] = 'DecryptionError'
|
|
3099
|
+
})(MessageRetryReason || (MessageRetryReason = {}))
|
|
3100
|
+
var MAC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3101
|
+
MessageRetryReason.SignalErrorBadMac,
|
|
3102
|
+
MessageRetryReason.SignalErrorInvalidMessage,
|
|
3103
|
+
MessageRetryReason.SignalErrorNoSession,
|
|
3104
|
+
MessageRetryReason.SignalErrorInvalidKeyId
|
|
3105
|
+
])
|
|
3106
|
+
function parseRetryReason(code) {
|
|
3107
|
+
if (code === void 0 || code === null) {
|
|
3108
|
+
return MessageRetryReason.UnknownError
|
|
3109
|
+
}
|
|
3110
|
+
const n = typeof code === 'string' ? parseInt(code, 10) : code
|
|
3111
|
+
if (isNaN(n)) {
|
|
3112
|
+
return MessageRetryReason.UnknownError
|
|
3113
|
+
}
|
|
3114
|
+
if (Object.values(MessageRetryReason).includes(n)) {
|
|
3115
|
+
return n
|
|
3116
|
+
}
|
|
3117
|
+
return MessageRetryReason.UnknownError
|
|
3118
|
+
}
|
|
3119
|
+
function isMacError(reason) {
|
|
3120
|
+
return MAC_ERROR_CODES.has(reason)
|
|
3121
|
+
}
|
|
3122
|
+
function getRetryReasonDescription(reason) {
|
|
3123
|
+
switch (reason) {
|
|
3124
|
+
case MessageRetryReason.UnknownError:
|
|
3125
|
+
return 'Unknown error'
|
|
3126
|
+
case MessageRetryReason.GenericError:
|
|
3127
|
+
return 'Generic error'
|
|
3128
|
+
case MessageRetryReason.SignalErrorInvalidKeyId:
|
|
3129
|
+
return 'Invalid key ID \u2014 peer prekey rotated'
|
|
3130
|
+
case MessageRetryReason.SignalErrorInvalidMessage:
|
|
3131
|
+
return 'Invalid message format'
|
|
3132
|
+
case MessageRetryReason.SignalErrorNoSession:
|
|
3133
|
+
return 'No session \u2014 peer not initialized'
|
|
3134
|
+
case MessageRetryReason.SignalErrorBadMac:
|
|
3135
|
+
return 'Bad MAC \u2014 encryption session mismatch'
|
|
3136
|
+
case MessageRetryReason.MessageExpired:
|
|
3137
|
+
return 'Message expired \u2014 too old to decrypt'
|
|
3138
|
+
case MessageRetryReason.DecryptionError:
|
|
3139
|
+
return 'Decryption failed'
|
|
3140
|
+
default:
|
|
3141
|
+
return `Unknown reason code ${reason}`
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
// wrapper.js
|
|
3146
|
+
function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
3147
|
+
const antiban = new AntiBan(config, warmUpState)
|
|
3148
|
+
const options = {
|
|
3149
|
+
autoRespondToIncoming: false,
|
|
3150
|
+
...wrapOptions
|
|
3151
|
+
}
|
|
3152
|
+
if (typeof sock.ev.process === 'function') {
|
|
3153
|
+
sock.ev.process(async events => {
|
|
3154
|
+
if (events['connection.update']) {
|
|
3155
|
+
const update = events['connection.update']
|
|
3156
|
+
if (update.connection === 'close') {
|
|
3157
|
+
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown'
|
|
3158
|
+
antiban.onDisconnect(reason)
|
|
3159
|
+
}
|
|
3160
|
+
if (update.connection === 'open') {
|
|
3161
|
+
antiban.onReconnect()
|
|
3162
|
+
}
|
|
3163
|
+
if (update.reachoutTimeLock) {
|
|
3164
|
+
antiban.timelock.onTimelockUpdate({
|
|
3165
|
+
isActive: update.reachoutTimeLock.isActive,
|
|
3166
|
+
timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
|
|
3167
|
+
enforcementType: update.reachoutTimeLock.enforcementType
|
|
3168
|
+
})
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
if (events['messages.update']) {
|
|
3172
|
+
const updates = events['messages.update']
|
|
3173
|
+
for (const update of updates) {
|
|
3174
|
+
if (update?.update?.messageStubParameters) {
|
|
3175
|
+
const params = update.update.messageStubParameters
|
|
3176
|
+
if (params.includes(463) || params.includes('463')) {
|
|
3177
|
+
antiban.timelock.record463Error()
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
antiban.retryTracker.onMessageUpdate(update)
|
|
3181
|
+
}
|
|
3182
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates)
|
|
3183
|
+
}
|
|
3184
|
+
if (events['messages.upsert']) {
|
|
3185
|
+
const { messages } = events['messages.upsert']
|
|
3186
|
+
antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert'])
|
|
3187
|
+
for (const msg of messages || []) {
|
|
3188
|
+
const jid = msg.key?.remoteJid
|
|
3189
|
+
if (!jid) continue
|
|
3190
|
+
antiban.timelock.registerKnownChat(jid)
|
|
3191
|
+
const isSelf = msg.key?.fromMe || false
|
|
3192
|
+
if (isSelf) continue
|
|
3193
|
+
const msgText =
|
|
3194
|
+
msg.message?.conversation ||
|
|
3195
|
+
msg.message?.extendedTextMessage?.text ||
|
|
3196
|
+
msg.message?.imageMessage?.caption ||
|
|
3197
|
+
msg.message?.videoMessage?.caption ||
|
|
3198
|
+
''
|
|
3199
|
+
const replySuggestion = antiban.onIncomingMessage(jid, msgText)
|
|
3200
|
+
if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
|
|
3201
|
+
const replyDelay = Math.floor(Math.random() * 12e3) + 3e3
|
|
3202
|
+
setTimeout(async () => {
|
|
3203
|
+
try {
|
|
3204
|
+
await sock.sendMessage(jid, { text: replySuggestion.suggestedText })
|
|
3205
|
+
} catch (error) {}
|
|
3206
|
+
}, replyDelay)
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
})
|
|
3211
|
+
} else {
|
|
3212
|
+
sock.ev.on('connection.update', update => {
|
|
3213
|
+
if (update.connection === 'close') {
|
|
3214
|
+
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown'
|
|
3215
|
+
antiban.onDisconnect(reason)
|
|
3216
|
+
}
|
|
3217
|
+
if (update.connection === 'open') {
|
|
3218
|
+
antiban.onReconnect()
|
|
3219
|
+
}
|
|
3220
|
+
if (update.reachoutTimeLock) {
|
|
3221
|
+
antiban.timelock.onTimelockUpdate({
|
|
3222
|
+
isActive: update.reachoutTimeLock.isActive,
|
|
3223
|
+
timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
|
|
3224
|
+
enforcementType: update.reachoutTimeLock.enforcementType
|
|
3225
|
+
})
|
|
3226
|
+
}
|
|
3227
|
+
})
|
|
3228
|
+
sock.ev.on('messages.update', updates => {
|
|
3229
|
+
for (const update of updates) {
|
|
3230
|
+
if (update?.update?.messageStubParameters) {
|
|
3231
|
+
const params = update.update.messageStubParameters
|
|
3232
|
+
if (params.includes(463) || params.includes('463')) {
|
|
3233
|
+
antiban.timelock.record463Error()
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
antiban.retryTracker.onMessageUpdate(update)
|
|
3237
|
+
}
|
|
3238
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates)
|
|
3239
|
+
})
|
|
3240
|
+
sock.ev.on('messages.upsert', upsert => {
|
|
3241
|
+
const { messages } = upsert
|
|
3242
|
+
antiban.jidCanonicalizer?.onIncomingEvent(upsert)
|
|
3243
|
+
for (const msg of messages || []) {
|
|
3244
|
+
const jid = msg.key?.remoteJid
|
|
3245
|
+
if (!jid) continue
|
|
3246
|
+
antiban.timelock.registerKnownChat(jid)
|
|
3247
|
+
const isSelf = msg.key?.fromMe || false
|
|
3248
|
+
if (isSelf) continue
|
|
3249
|
+
const msgText =
|
|
3250
|
+
msg.message?.conversation ||
|
|
3251
|
+
msg.message?.extendedTextMessage?.text ||
|
|
3252
|
+
msg.message?.imageMessage?.caption ||
|
|
3253
|
+
msg.message?.videoMessage?.caption ||
|
|
3254
|
+
''
|
|
3255
|
+
const replySuggestion = antiban.onIncomingMessage(jid, msgText)
|
|
3256
|
+
if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
|
|
3257
|
+
const replyDelay = Math.floor(Math.random() * 12e3) + 3e3
|
|
3258
|
+
setTimeout(async () => {
|
|
3259
|
+
try {
|
|
3260
|
+
await sock.sendMessage(jid, { text: replySuggestion.suggestedText })
|
|
3261
|
+
} catch (error) {}
|
|
3262
|
+
}, replyDelay)
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
})
|
|
3266
|
+
}
|
|
3267
|
+
const originalSendMessage = sock.sendMessage.bind(sock)
|
|
3268
|
+
const wrappedSendMessage = async (jid, content, options2) => {
|
|
3269
|
+
const canonicalJid = antiban.jidCanonicalizer?.canonicalizeTarget(jid) || jid
|
|
3270
|
+
const text = content?.text || content?.caption || content?.image?.caption || ''
|
|
3271
|
+
const decision = await antiban.beforeSend(canonicalJid, text)
|
|
3272
|
+
if (!decision.allowed) {
|
|
3273
|
+
throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`)
|
|
3274
|
+
}
|
|
3275
|
+
if (decision.delayMs > 0) {
|
|
3276
|
+
await new Promise(resolve => setTimeout(resolve, decision.delayMs))
|
|
3277
|
+
}
|
|
3278
|
+
try {
|
|
3279
|
+
const result = await originalSendMessage(canonicalJid, content, options2)
|
|
3280
|
+
antiban.afterSend(canonicalJid, text)
|
|
3281
|
+
antiban.timelock.registerKnownChat(canonicalJid)
|
|
3282
|
+
if (result?.key?.id) {
|
|
3283
|
+
antiban.retryTracker.clear(result.key.id)
|
|
3284
|
+
}
|
|
3285
|
+
return result
|
|
3286
|
+
} catch (error) {
|
|
3287
|
+
antiban.afterSendFailed(error instanceof Error ? error.message : String(error))
|
|
3288
|
+
throw error
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
const wrapped = Object.create(sock)
|
|
3292
|
+
wrapped.sendMessage = wrappedSendMessage
|
|
3293
|
+
wrapped.antiban = antiban
|
|
3294
|
+
wrapped.antiban.destroy = antiban.destroy.bind(antiban)
|
|
3295
|
+
return wrapped
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// messageQueue.js
|
|
3299
|
+
var import_events = require('events')
|
|
3300
|
+
var DEFAULT_CONFIG12 = {
|
|
3301
|
+
maxAttempts: 3,
|
|
3302
|
+
retryBaseDelayMs: 3e4,
|
|
3303
|
+
maxQueueSize: 1e3,
|
|
3304
|
+
priorityOrder: true
|
|
3305
|
+
}
|
|
3306
|
+
var MessageQueue = class extends import_events.EventEmitter {
|
|
3307
|
+
config
|
|
3308
|
+
queue = []
|
|
3309
|
+
processing = false
|
|
3310
|
+
sendFn = null
|
|
3311
|
+
drainTimer = null
|
|
3312
|
+
idCounter = 0
|
|
3313
|
+
constructor(config = {}) {
|
|
3314
|
+
super()
|
|
3315
|
+
this.config = { ...DEFAULT_CONFIG12, ...config }
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Set the send function (called for each message when drained)
|
|
3319
|
+
* This should be the anti-ban wrapped sendMessage
|
|
3320
|
+
*/
|
|
3321
|
+
setSendFunction(fn) {
|
|
3322
|
+
this.sendFn = fn
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Add a message to the queue
|
|
3326
|
+
*/
|
|
3327
|
+
add(recipient, content, options) {
|
|
3328
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
3329
|
+
throw new Error(`Queue full (${this.config.maxQueueSize} messages)`)
|
|
3330
|
+
}
|
|
3331
|
+
const id = `msg_${Date.now()}_${++this.idCounter}`
|
|
3332
|
+
const message = {
|
|
3333
|
+
id,
|
|
3334
|
+
recipient,
|
|
3335
|
+
content,
|
|
3336
|
+
priority: options?.priority || 'normal',
|
|
3337
|
+
addedAt: Date.now(),
|
|
3338
|
+
attempts: 0,
|
|
3339
|
+
maxAttempts: this.config.maxAttempts,
|
|
3340
|
+
scheduledFor: options?.scheduledFor?.getTime(),
|
|
3341
|
+
metadata: options?.metadata
|
|
3342
|
+
}
|
|
3343
|
+
this.queue.push(message)
|
|
3344
|
+
this.sortQueue()
|
|
3345
|
+
this.emit('added', message)
|
|
3346
|
+
return id
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Add multiple messages (e.g., broadcast to many recipients)
|
|
3350
|
+
*/
|
|
3351
|
+
addBulk(recipients, content, options) {
|
|
3352
|
+
return recipients.map(r => this.add(r, content, options))
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Start processing the queue
|
|
3356
|
+
*/
|
|
3357
|
+
start(intervalMs = 1e3) {
|
|
3358
|
+
if (this.drainTimer) return
|
|
3359
|
+
this.drainTimer = setInterval(() => this.processNext(), intervalMs)
|
|
3360
|
+
this.emit('started')
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Stop processing
|
|
3364
|
+
*/
|
|
3365
|
+
stop() {
|
|
3366
|
+
if (this.drainTimer) {
|
|
3367
|
+
clearInterval(this.drainTimer)
|
|
3368
|
+
this.drainTimer = null
|
|
3369
|
+
}
|
|
3370
|
+
this.emit('stopped')
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Clean up all timers and resources.
|
|
3374
|
+
* Call this when disposing of the queue.
|
|
3375
|
+
*/
|
|
3376
|
+
destroy() {
|
|
3377
|
+
this.stop()
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Process the next message in the queue
|
|
3381
|
+
*/
|
|
3382
|
+
async processNext() {
|
|
3383
|
+
if (this.processing || !this.sendFn) return
|
|
3384
|
+
const now = Date.now()
|
|
3385
|
+
const message = this.queue.find(m => !m.scheduledFor || m.scheduledFor <= now)
|
|
3386
|
+
if (!message) return
|
|
3387
|
+
this.processing = true
|
|
3388
|
+
try {
|
|
3389
|
+
message.attempts++
|
|
3390
|
+
await this.sendFn(message.recipient, message.content)
|
|
3391
|
+
this.queue = this.queue.filter(m => m.id !== message.id)
|
|
3392
|
+
this.emit('sent', message)
|
|
3393
|
+
} catch (err) {
|
|
3394
|
+
message.lastError = err.message
|
|
3395
|
+
if (err.message?.includes('baileys-antiban')) {
|
|
3396
|
+
message.attempts--
|
|
3397
|
+
this.emit('delayed', message, err.message)
|
|
3398
|
+
} else if (message.attempts >= message.maxAttempts) {
|
|
3399
|
+
this.queue = this.queue.filter(m => m.id !== message.id)
|
|
3400
|
+
this.emit('failed', message, err.message)
|
|
3401
|
+
} else {
|
|
3402
|
+
const backoff = this.config.retryBaseDelayMs * Math.pow(2, message.attempts - 1)
|
|
3403
|
+
message.scheduledFor = Date.now() + backoff
|
|
3404
|
+
this.emit('retry', message, message.attempts, backoff)
|
|
3405
|
+
}
|
|
3406
|
+
} finally {
|
|
3407
|
+
this.processing = false
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Get queue stats
|
|
3412
|
+
*/
|
|
3413
|
+
getStats() {
|
|
3414
|
+
const now = Date.now()
|
|
3415
|
+
return {
|
|
3416
|
+
total: this.queue.length,
|
|
3417
|
+
pending: this.queue.filter(m => !m.scheduledFor || m.scheduledFor <= now).length,
|
|
3418
|
+
scheduled: this.queue.filter(m => m.scheduledFor && m.scheduledFor > now).length,
|
|
3419
|
+
byPriority: {
|
|
3420
|
+
high: this.queue.filter(m => m.priority === 'high').length,
|
|
3421
|
+
normal: this.queue.filter(m => m.priority === 'normal').length,
|
|
3422
|
+
low: this.queue.filter(m => m.priority === 'low').length
|
|
3423
|
+
},
|
|
3424
|
+
processing: this.processing,
|
|
3425
|
+
isRunning: this.drainTimer !== null
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
/**
|
|
3429
|
+
* Clear all messages
|
|
3430
|
+
*/
|
|
3431
|
+
clear() {
|
|
3432
|
+
const count = this.queue.length
|
|
3433
|
+
this.queue = []
|
|
3434
|
+
this.emit('cleared', count)
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Remove a specific message
|
|
3438
|
+
*/
|
|
3439
|
+
remove(id) {
|
|
3440
|
+
const before = this.queue.length
|
|
3441
|
+
this.queue = this.queue.filter(m => m.id !== id)
|
|
3442
|
+
return this.queue.length < before
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Export queue for persistence
|
|
3446
|
+
*/
|
|
3447
|
+
export() {
|
|
3448
|
+
return [...this.queue]
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* Import queue (e.g., after restart)
|
|
3452
|
+
*/
|
|
3453
|
+
import(messages) {
|
|
3454
|
+
this.queue = [...messages]
|
|
3455
|
+
this.sortQueue()
|
|
3456
|
+
}
|
|
3457
|
+
sortQueue() {
|
|
3458
|
+
if (!this.config.priorityOrder) return
|
|
3459
|
+
const priorityWeight = { high: 0, normal: 1, low: 2 }
|
|
3460
|
+
this.queue.sort((a, b) => {
|
|
3461
|
+
const pDiff = priorityWeight[a.priority] - priorityWeight[b.priority]
|
|
3462
|
+
if (pDiff !== 0) return pDiff
|
|
3463
|
+
return a.addedAt - b.addedAt
|
|
3464
|
+
})
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// contentVariator.js
|
|
3469
|
+
var DEFAULT_CONFIG13 = {
|
|
3470
|
+
zeroWidthChars: true,
|
|
3471
|
+
punctuationVariation: true,
|
|
3472
|
+
emojiPadding: false,
|
|
3473
|
+
synonyms: false
|
|
3474
|
+
}
|
|
3475
|
+
var ZERO_WIDTH = [
|
|
3476
|
+
'\u200B',
|
|
3477
|
+
// zero-width space
|
|
3478
|
+
'\u200C',
|
|
3479
|
+
// zero-width non-joiner
|
|
3480
|
+
'\u200D',
|
|
3481
|
+
// zero-width joiner
|
|
3482
|
+
'\uFEFF'
|
|
3483
|
+
// zero-width no-break space
|
|
3484
|
+
]
|
|
3485
|
+
var SYNONYMS = {
|
|
3486
|
+
hello: ['hi', 'hey', 'howdy'],
|
|
3487
|
+
hi: ['hello', 'hey', 'howdy'],
|
|
3488
|
+
thanks: ['thank you', 'thx', 'cheers'],
|
|
3489
|
+
please: ['kindly', 'pls'],
|
|
3490
|
+
great: ['awesome', 'excellent', 'wonderful'],
|
|
3491
|
+
good: ['great', 'nice', 'fine'],
|
|
3492
|
+
buy: ['purchase', 'get', 'grab'],
|
|
3493
|
+
sell: ['offer', 'list'],
|
|
3494
|
+
price: ['cost', 'amount', 'value'],
|
|
3495
|
+
available: ['in stock', 'on offer'],
|
|
3496
|
+
check: ['look at', 'see', 'view'],
|
|
3497
|
+
join: ['participate', 'enter', 'come to'],
|
|
3498
|
+
start: ['begin', 'kick off', 'commence'],
|
|
3499
|
+
end: ['finish', 'close', 'conclude'],
|
|
3500
|
+
bid: ['offer', 'place a bid'],
|
|
3501
|
+
win: ['secure', 'take home'],
|
|
3502
|
+
item: ['lot', 'piece', 'product']
|
|
3503
|
+
}
|
|
3504
|
+
var ContentVariator = class {
|
|
3505
|
+
config
|
|
3506
|
+
counter = 0
|
|
3507
|
+
constructor(config = {}) {
|
|
3508
|
+
this.config = { ...DEFAULT_CONFIG13, ...config }
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* Create a unique variation of a message
|
|
3512
|
+
* Each call produces a slightly different version
|
|
3513
|
+
*/
|
|
3514
|
+
vary(text) {
|
|
3515
|
+
let result = text
|
|
3516
|
+
this.counter++
|
|
3517
|
+
if (this.config.customVariator) {
|
|
3518
|
+
return this.config.customVariator(result, this.counter)
|
|
3519
|
+
}
|
|
3520
|
+
if (this.config.synonyms) {
|
|
3521
|
+
result = this.applySynonyms(result)
|
|
3522
|
+
}
|
|
3523
|
+
if (this.config.zeroWidthChars) {
|
|
3524
|
+
result = this.addZeroWidth(result)
|
|
3525
|
+
}
|
|
3526
|
+
if (this.config.punctuationVariation) {
|
|
3527
|
+
result = this.varyPunctuation(result)
|
|
3528
|
+
}
|
|
3529
|
+
if (this.config.emojiPadding) {
|
|
3530
|
+
result = this.addEmojiPadding(result)
|
|
3531
|
+
}
|
|
3532
|
+
return result
|
|
3533
|
+
}
|
|
3534
|
+
/**
|
|
3535
|
+
* Create N unique variations of a message
|
|
3536
|
+
*/
|
|
3537
|
+
varyBulk(text, count) {
|
|
3538
|
+
const results = []
|
|
3539
|
+
const seen = /* @__PURE__ */ new Set()
|
|
3540
|
+
for (let i = 0; i < count; i++) {
|
|
3541
|
+
let variation = this.vary(text)
|
|
3542
|
+
let attempts = 0
|
|
3543
|
+
while (seen.has(variation) && attempts < 10) {
|
|
3544
|
+
variation = this.vary(text)
|
|
3545
|
+
attempts++
|
|
3546
|
+
}
|
|
3547
|
+
seen.add(variation)
|
|
3548
|
+
results.push(variation)
|
|
3549
|
+
}
|
|
3550
|
+
return results
|
|
3551
|
+
}
|
|
3552
|
+
addZeroWidth(text) {
|
|
3553
|
+
const words = text.split(' ')
|
|
3554
|
+
if (words.length < 2) return text
|
|
3555
|
+
const positions = this.randomPositions(words.length - 1, Math.min(2, words.length - 1))
|
|
3556
|
+
return words
|
|
3557
|
+
.map((word, i) => {
|
|
3558
|
+
if (positions.includes(i)) {
|
|
3559
|
+
const zwc = ZERO_WIDTH[Math.floor(Math.random() * ZERO_WIDTH.length)]
|
|
3560
|
+
return word + zwc
|
|
3561
|
+
}
|
|
3562
|
+
return word
|
|
3563
|
+
})
|
|
3564
|
+
.join(' ')
|
|
3565
|
+
}
|
|
3566
|
+
varyPunctuation(text) {
|
|
3567
|
+
const variations = [
|
|
3568
|
+
// Trailing space variations
|
|
3569
|
+
() => text + ' ',
|
|
3570
|
+
() => text + ' ',
|
|
3571
|
+
// Period variations
|
|
3572
|
+
() => (text.endsWith('.') ? text.slice(0, -1) : text + '.'),
|
|
3573
|
+
// Nothing
|
|
3574
|
+
() => text,
|
|
3575
|
+
// Capitalize first letter variation
|
|
3576
|
+
() => (text.charAt(0) === text.charAt(0).toUpperCase() ? text.charAt(0).toLowerCase() + text.slice(1) : text)
|
|
3577
|
+
]
|
|
3578
|
+
return variations[this.counter % variations.length]()
|
|
3579
|
+
}
|
|
3580
|
+
addEmojiPadding(text) {
|
|
3581
|
+
const emojis = ['', ' \u{1F44D}', ' \u2705', ' \u{1F4CC}', ' \u{1F4AC}', ' \u{1F4E2}']
|
|
3582
|
+
return text + emojis[this.counter % emojis.length]
|
|
3583
|
+
}
|
|
3584
|
+
applySynonyms(text) {
|
|
3585
|
+
const words = text.split(/\b/)
|
|
3586
|
+
let replaced = false
|
|
3587
|
+
return words
|
|
3588
|
+
.map(word => {
|
|
3589
|
+
if (replaced) return word
|
|
3590
|
+
const lower = word.toLowerCase()
|
|
3591
|
+
const synonymList = SYNONYMS[lower]
|
|
3592
|
+
if (synonymList && Math.random() > 0.5) {
|
|
3593
|
+
replaced = true
|
|
3594
|
+
const synonym = synonymList[Math.floor(Math.random() * synonymList.length)]
|
|
3595
|
+
return word[0] === word[0].toUpperCase() ? synonym.charAt(0).toUpperCase() + synonym.slice(1) : synonym
|
|
3596
|
+
}
|
|
3597
|
+
return word
|
|
3598
|
+
})
|
|
3599
|
+
.join('')
|
|
3600
|
+
}
|
|
3601
|
+
randomPositions(max, count) {
|
|
3602
|
+
const positions = []
|
|
3603
|
+
while (positions.length < count) {
|
|
3604
|
+
const pos = Math.floor(Math.random() * max)
|
|
3605
|
+
if (!positions.includes(pos)) positions.push(pos)
|
|
3606
|
+
}
|
|
3607
|
+
return positions
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// webhooks.js
|
|
3612
|
+
var DEFAULT_CONFIG14 = {
|
|
3613
|
+
urls: [],
|
|
3614
|
+
minRiskLevel: 'medium',
|
|
3615
|
+
cooldownMs: 3e5,
|
|
3616
|
+
includeStats: true
|
|
3617
|
+
}
|
|
3618
|
+
var WebhookAlerts = class {
|
|
3619
|
+
config
|
|
3620
|
+
lastAlertTime = 0
|
|
3621
|
+
constructor(config = {}) {
|
|
3622
|
+
this.config = { ...DEFAULT_CONFIG14, ...config }
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Send alert if risk level warrants it
|
|
3626
|
+
*/
|
|
3627
|
+
async alert(data) {
|
|
3628
|
+
const riskOrder = ['low', 'medium', 'high', 'critical']
|
|
3629
|
+
if (riskOrder.indexOf(data.risk) < riskOrder.indexOf(this.config.minRiskLevel)) {
|
|
3630
|
+
return
|
|
3631
|
+
}
|
|
3632
|
+
const now = Date.now()
|
|
3633
|
+
if (now - this.lastAlertTime < this.config.cooldownMs) {
|
|
3634
|
+
return
|
|
3635
|
+
}
|
|
3636
|
+
this.lastAlertTime = now
|
|
3637
|
+
const payload = {
|
|
3638
|
+
source: 'baileys-antiban',
|
|
3639
|
+
timestamp: /* @__PURE__ */ new Date().toISOString(),
|
|
3640
|
+
...data
|
|
3641
|
+
}
|
|
3642
|
+
for (const url of this.config.urls) {
|
|
3643
|
+
this.postWebhook(url, payload).catch(() => {})
|
|
3644
|
+
}
|
|
3645
|
+
if (this.config.telegram) {
|
|
3646
|
+
const emoji =
|
|
3647
|
+
{ low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' }[data.risk] || '\u26AA'
|
|
3648
|
+
const text = `${emoji} *baileys-antiban Alert*
|
|
3649
|
+
|
|
3650
|
+
Risk: *${data.risk.toUpperCase()}* (score: ${data.score})
|
|
3651
|
+
${data.recommendation}
|
|
3652
|
+
|
|
3653
|
+
Reasons:
|
|
3654
|
+
${data.reasons.map(r => `\u2022 ${r}`).join('\n')}`
|
|
3655
|
+
this.postWebhook(`https://api.telegram.org/bot${this.config.telegram.botToken}/sendMessage`, {
|
|
3656
|
+
chat_id: this.config.telegram.chatId,
|
|
3657
|
+
text,
|
|
3658
|
+
parse_mode: 'Markdown'
|
|
3659
|
+
}).catch(() => {})
|
|
3660
|
+
}
|
|
3661
|
+
if (this.config.discord) {
|
|
3662
|
+
const color = { low: 65280, medium: 16776960, high: 16746496, critical: 16711680 }[data.risk] || 0
|
|
3663
|
+
this.postWebhook(this.config.discord.webhookUrl, {
|
|
3664
|
+
embeds: [
|
|
3665
|
+
{
|
|
3666
|
+
title: '\u{1F6E1}\uFE0F baileys-antiban Alert',
|
|
3667
|
+
color,
|
|
3668
|
+
fields: [
|
|
3669
|
+
{ name: 'Risk', value: data.risk.toUpperCase(), inline: true },
|
|
3670
|
+
{ name: 'Score', value: String(data.score), inline: true },
|
|
3671
|
+
{ name: 'Recommendation', value: data.recommendation },
|
|
3672
|
+
{ name: 'Reasons', value: data.reasons.join('\n') }
|
|
3673
|
+
],
|
|
3674
|
+
timestamp: /* @__PURE__ */ new Date().toISOString()
|
|
3675
|
+
}
|
|
3676
|
+
]
|
|
3677
|
+
}).catch(() => {})
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
async postWebhook(url, payload) {
|
|
3681
|
+
try {
|
|
3682
|
+
const response = await fetch(url, {
|
|
3683
|
+
method: 'POST',
|
|
3684
|
+
headers: {
|
|
3685
|
+
'Content-Type': 'application/json',
|
|
3686
|
+
...this.config.headers
|
|
3687
|
+
},
|
|
3688
|
+
body: JSON.stringify(payload)
|
|
3689
|
+
})
|
|
3690
|
+
if (!response.ok) {
|
|
3691
|
+
process.stderr.write(`[baileys-antiban] Webhook failed: ${response.status}\n`)
|
|
3692
|
+
}
|
|
3693
|
+
} catch (err) {
|
|
3694
|
+
process.stderr.write(`[baileys-antiban] Webhook error: ${err}\n`)
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
// scheduler.js
|
|
3700
|
+
var DEFAULT_CONFIG15 = {
|
|
3701
|
+
timezone: 'UTC',
|
|
3702
|
+
activeHours: [8, 21],
|
|
3703
|
+
weekendFactor: 0.5,
|
|
3704
|
+
peakHours: [10, 14],
|
|
3705
|
+
peakFactor: 1.3,
|
|
3706
|
+
lunchBreak: [12, 13],
|
|
3707
|
+
lunchFactor: 0.5
|
|
3708
|
+
}
|
|
3709
|
+
var Scheduler = class {
|
|
3710
|
+
config
|
|
3711
|
+
constructor(config = {}) {
|
|
3712
|
+
this.config = { ...DEFAULT_CONFIG15, ...config }
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* Check if now is within active hours
|
|
3716
|
+
*/
|
|
3717
|
+
isActiveTime() {
|
|
3718
|
+
const hour = this.getCurrentHour()
|
|
3719
|
+
const [start, end] = this.config.activeHours
|
|
3720
|
+
return hour >= start && hour < end
|
|
3721
|
+
}
|
|
3722
|
+
/**
|
|
3723
|
+
* Get the speed multiplier for current time
|
|
3724
|
+
* > 1 = faster, < 1 = slower, 0 = don't send
|
|
3725
|
+
*/
|
|
3726
|
+
getSpeedFactor() {
|
|
3727
|
+
if (!this.isActiveTime()) return 0
|
|
3728
|
+
const hour = this.getCurrentHour()
|
|
3729
|
+
const day = this.getCurrentDay()
|
|
3730
|
+
let factor = 1
|
|
3731
|
+
if (day === 0 || day === 6) {
|
|
3732
|
+
factor *= this.config.weekendFactor
|
|
3733
|
+
}
|
|
3734
|
+
const [peakStart, peakEnd] = this.config.peakHours
|
|
3735
|
+
if (hour >= peakStart && hour < peakEnd) {
|
|
3736
|
+
factor *= this.config.peakFactor
|
|
3737
|
+
}
|
|
3738
|
+
const [lunchStart, lunchEnd] = this.config.lunchBreak
|
|
3739
|
+
if (hour >= lunchStart && hour < lunchEnd) {
|
|
3740
|
+
factor *= this.config.lunchFactor
|
|
3741
|
+
}
|
|
3742
|
+
return factor
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Get ms until next active window
|
|
3746
|
+
*/
|
|
3747
|
+
msUntilActive() {
|
|
3748
|
+
if (this.isActiveTime()) return 0
|
|
3749
|
+
const now = /* @__PURE__ */ new Date()
|
|
3750
|
+
const hour = now.getHours()
|
|
3751
|
+
const [start] = this.config.activeHours
|
|
3752
|
+
let nextActive
|
|
3753
|
+
if (hour >= this.config.activeHours[1]) {
|
|
3754
|
+
nextActive = new Date(now)
|
|
3755
|
+
nextActive.setDate(nextActive.getDate() + 1)
|
|
3756
|
+
nextActive.setHours(start, 0, 0, 0)
|
|
3757
|
+
} else {
|
|
3758
|
+
nextActive = new Date(now)
|
|
3759
|
+
nextActive.setHours(start, 0, 0, 0)
|
|
3760
|
+
}
|
|
3761
|
+
return nextActive.getTime() - now.getTime()
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Adjust a delay based on current time factors
|
|
3765
|
+
*/
|
|
3766
|
+
adjustDelay(baseDelayMs) {
|
|
3767
|
+
const factor = this.getSpeedFactor()
|
|
3768
|
+
if (factor === 0) return -1
|
|
3769
|
+
return Math.round(baseDelayMs / factor)
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Get current schedule status
|
|
3773
|
+
*/
|
|
3774
|
+
getStatus() {
|
|
3775
|
+
const hour = this.getCurrentHour()
|
|
3776
|
+
const day = this.getCurrentDay()
|
|
3777
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
|
|
3778
|
+
return {
|
|
3779
|
+
active: this.isActiveTime(),
|
|
3780
|
+
currentHour: hour,
|
|
3781
|
+
day: dayNames[day],
|
|
3782
|
+
isWeekend: day === 0 || day === 6,
|
|
3783
|
+
speedFactor: this.getSpeedFactor(),
|
|
3784
|
+
msUntilActive: this.msUntilActive(),
|
|
3785
|
+
activeWindow: `${this.config.activeHours[0]}:00 - ${this.config.activeHours[1]}:00`
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
getCurrentHour() {
|
|
3789
|
+
return /* @__PURE__ */ new Date().getHours()
|
|
3790
|
+
}
|
|
3791
|
+
getCurrentDay() {
|
|
3792
|
+
return /* @__PURE__ */ new Date().getDay()
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
// stateAdapter.js
|
|
3797
|
+
var _fsPromises = require('fs/promises')
|
|
3798
|
+
var _fsSync = require('fs')
|
|
3799
|
+
var _path3 = require('path')
|
|
3800
|
+
var FileStateAdapter = class {
|
|
3801
|
+
basePath
|
|
3802
|
+
constructor(basePath) {
|
|
3803
|
+
this.basePath = basePath
|
|
3804
|
+
}
|
|
3805
|
+
async save(key, state) {
|
|
3806
|
+
const filePath = _path3.join(this.basePath, `${key}.json`)
|
|
3807
|
+
await _fsPromises.mkdir(this.basePath, { recursive: true })
|
|
3808
|
+
await _fsPromises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8')
|
|
3809
|
+
}
|
|
3810
|
+
async load(key) {
|
|
3811
|
+
const filePath = _path3.join(this.basePath, `${key}.json`)
|
|
3812
|
+
try {
|
|
3813
|
+
const data = await _fsPromises.readFile(filePath, 'utf-8')
|
|
3814
|
+
return JSON.parse(data)
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
if (err.code === 'ENOENT') return null
|
|
3817
|
+
throw err
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
async delete(key) {
|
|
3821
|
+
const filePath = _path3.join(this.basePath, `${key}.json`)
|
|
3822
|
+
try {
|
|
3823
|
+
await _fsPromises.unlink(filePath)
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
if (err.code !== 'ENOENT') throw err
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
async list() {
|
|
3829
|
+
try {
|
|
3830
|
+
const files = await _fsPromises.readdir(this.basePath)
|
|
3831
|
+
return files.filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''))
|
|
3832
|
+
} catch (err) {
|
|
3833
|
+
if (err.code === 'ENOENT') return []
|
|
3834
|
+
throw err
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
// messageRecovery.js
|
|
3840
|
+
var DEFAULT_CONFIG16 = {
|
|
3841
|
+
maxTrackedChats: 1e3,
|
|
3842
|
+
maxGapMs: 30 * 6e4,
|
|
3843
|
+
// 30 minutes
|
|
3844
|
+
persistDebounceMs: 2e3,
|
|
3845
|
+
onGapFilled: () => {},
|
|
3846
|
+
logger: {
|
|
3847
|
+
info: () => {},
|
|
3848
|
+
warn: () => {},
|
|
3849
|
+
error: () => {}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
function messageRecovery(sock, config) {
|
|
3853
|
+
const cfg = { ...DEFAULT_CONFIG16, ...config }
|
|
3854
|
+
const logger = cfg.logger
|
|
3855
|
+
const lastSeen = /* @__PURE__ */ new Map()
|
|
3856
|
+
let disconnectedAt = null
|
|
3857
|
+
let totalRecovered = 0
|
|
3858
|
+
let lastReconnectAt = null
|
|
3859
|
+
let lastGapMs = null
|
|
3860
|
+
let persistTimer = null
|
|
3861
|
+
let loggedFetchWarning = false
|
|
3862
|
+
if (cfg.persistPath) {
|
|
3863
|
+
loadPersistence()
|
|
3864
|
+
}
|
|
3865
|
+
const messagesListener = sock.ev.process ? setupProcessListener() : setupLegacyListener()
|
|
3866
|
+
const connectionListener = update => {
|
|
3867
|
+
if (update.connection === 'close') {
|
|
3868
|
+
disconnectedAt = Date.now()
|
|
3869
|
+
logger.info?.(`[messageRecovery] Disconnected at ${new Date(disconnectedAt).toISOString()}`)
|
|
3870
|
+
}
|
|
3871
|
+
if (update.connection === 'open' && disconnectedAt !== null) {
|
|
3872
|
+
void recoverMessages()
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
sock.ev.on('connection.update', connectionListener)
|
|
3876
|
+
function setupProcessListener() {
|
|
3877
|
+
const listener = async events => {
|
|
3878
|
+
if (events['messages.upsert']) {
|
|
3879
|
+
const { messages, type } = events['messages.upsert']
|
|
3880
|
+
if (type === 'notify') {
|
|
3881
|
+
for (const msg of messages || []) {
|
|
3882
|
+
trackMessage(msg)
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
sock.ev.process(listener)
|
|
3888
|
+
return listener
|
|
3889
|
+
}
|
|
3890
|
+
function setupLegacyListener() {
|
|
3891
|
+
const listener = upsert => {
|
|
3892
|
+
const { messages, type } = upsert
|
|
3893
|
+
if (type === 'notify') {
|
|
3894
|
+
for (const msg of messages || []) {
|
|
3895
|
+
trackMessage(msg)
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
sock.ev.on('messages.upsert', listener)
|
|
3900
|
+
return listener
|
|
3901
|
+
}
|
|
3902
|
+
function trackMessage(msg) {
|
|
3903
|
+
const jid = msg.key?.remoteJid
|
|
3904
|
+
const messageId = msg.key?.id
|
|
3905
|
+
const timestamp = msg.messageTimestamp
|
|
3906
|
+
if (!jid || !messageId || !timestamp) return
|
|
3907
|
+
if (msg.key?.fromMe) return
|
|
3908
|
+
const now = Date.now()
|
|
3909
|
+
lastSeen.set(jid, {
|
|
3910
|
+
messageId,
|
|
3911
|
+
timestamp: typeof timestamp === 'number' ? timestamp : parseInt(timestamp, 10),
|
|
3912
|
+
lastTouchedAt: now
|
|
3913
|
+
})
|
|
3914
|
+
if (lastSeen.size > cfg.maxTrackedChats) {
|
|
3915
|
+
evictOldest()
|
|
3916
|
+
}
|
|
3917
|
+
schedulePersist()
|
|
3918
|
+
}
|
|
3919
|
+
function evictOldest() {
|
|
3920
|
+
let oldestJid = null
|
|
3921
|
+
let oldestTime = Infinity
|
|
3922
|
+
for (const [jid, entry] of lastSeen) {
|
|
3923
|
+
if (entry.lastTouchedAt < oldestTime) {
|
|
3924
|
+
oldestTime = entry.lastTouchedAt
|
|
3925
|
+
oldestJid = jid
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
if (oldestJid) {
|
|
3929
|
+
lastSeen.delete(oldestJid)
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
async function recoverMessages() {
|
|
3933
|
+
const recoveryStartMs = Date.now()
|
|
3934
|
+
const gapMs = recoveryStartMs - disconnectedAt
|
|
3935
|
+
logger.info?.(`[messageRecovery] Reconnected after ${(gapMs / 1e3).toFixed(1)}s`)
|
|
3936
|
+
if (gapMs > cfg.maxGapMs) {
|
|
3937
|
+
logger.warn?.(
|
|
3938
|
+
`[messageRecovery] Gap too large (${(gapMs / 1e3).toFixed(0)}s > ${(cfg.maxGapMs / 1e3).toFixed(0)}s) \u2014 skipping recovery`
|
|
3939
|
+
)
|
|
3940
|
+
disconnectedAt = null
|
|
3941
|
+
lastGapMs = gapMs
|
|
3942
|
+
await cfg.onGapTooLarge?.(gapMs)
|
|
3943
|
+
return
|
|
3944
|
+
}
|
|
3945
|
+
let recovered = 0
|
|
3946
|
+
const chatsToRecover = Array.from(lastSeen.entries())
|
|
3947
|
+
if (typeof sock.fetchMessageHistory !== 'function') {
|
|
3948
|
+
if (!loggedFetchWarning) {
|
|
3949
|
+
logger.warn?.(
|
|
3950
|
+
`[messageRecovery] sock.fetchMessageHistory not available \u2014 recovery disabled. Baileys version may not support history fetch. User must implement manual reconciliation.`
|
|
3951
|
+
)
|
|
3952
|
+
loggedFetchWarning = true
|
|
3953
|
+
}
|
|
3954
|
+
disconnectedAt = null
|
|
3955
|
+
lastReconnectAt = /* @__PURE__ */ new Date()
|
|
3956
|
+
lastGapMs = gapMs
|
|
3957
|
+
await cfg.onRecoveryComplete?.({
|
|
3958
|
+
chats: 0,
|
|
3959
|
+
recovered: 0,
|
|
3960
|
+
durationMs: Date.now() - recoveryStartMs
|
|
3961
|
+
})
|
|
3962
|
+
return
|
|
3963
|
+
}
|
|
3964
|
+
for (const [jid, lastSeenEntry] of chatsToRecover) {
|
|
3965
|
+
try {
|
|
3966
|
+
const messages = await sock.fetchMessageHistory(jid, 50, {
|
|
3967
|
+
before: void 0
|
|
3968
|
+
// Get latest
|
|
3969
|
+
})
|
|
3970
|
+
if (!messages || !Array.isArray(messages)) continue
|
|
3971
|
+
const gapMessages = messages.filter(msg => {
|
|
3972
|
+
const ts = msg.messageTimestamp
|
|
3973
|
+
if (!ts) return false
|
|
3974
|
+
const msgTs = typeof ts === 'number' ? ts : parseInt(ts, 10)
|
|
3975
|
+
return msgTs > lastSeenEntry.timestamp
|
|
3976
|
+
})
|
|
3977
|
+
gapMessages.sort((a, b) => {
|
|
3978
|
+
const aTs = typeof a.messageTimestamp === 'number' ? a.messageTimestamp : parseInt(a.messageTimestamp, 10)
|
|
3979
|
+
const bTs = typeof b.messageTimestamp === 'number' ? b.messageTimestamp : parseInt(b.messageTimestamp, 10)
|
|
3980
|
+
return aTs - bTs
|
|
3981
|
+
})
|
|
3982
|
+
for (const msg of gapMessages) {
|
|
3983
|
+
await cfg.onGapFilled(msg, jid)
|
|
3984
|
+
recovered++
|
|
3985
|
+
const msgTs =
|
|
3986
|
+
typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : parseInt(msg.messageTimestamp, 10)
|
|
3987
|
+
if (msgTs > lastSeenEntry.timestamp) {
|
|
3988
|
+
lastSeenEntry.timestamp = msgTs
|
|
3989
|
+
lastSeenEntry.messageId = msg.key?.id || lastSeenEntry.messageId
|
|
3990
|
+
lastSeenEntry.lastTouchedAt = Date.now()
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
if (gapMessages.length > 0) {
|
|
3994
|
+
logger.info?.(`[messageRecovery] Recovered ${gapMessages.length} messages from ${jid}`)
|
|
3995
|
+
}
|
|
3996
|
+
} catch (err) {
|
|
3997
|
+
logger.error?.(`[messageRecovery] Failed to recover from ${jid}: ${err.message}`)
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
totalRecovered += recovered
|
|
4001
|
+
lastReconnectAt = /* @__PURE__ */ new Date()
|
|
4002
|
+
lastGapMs = gapMs
|
|
4003
|
+
disconnectedAt = null
|
|
4004
|
+
logger.info?.(
|
|
4005
|
+
`[messageRecovery] Recovery complete: ${recovered} messages across ${chatsToRecover.length} chats in ${Date.now() - recoveryStartMs}ms`
|
|
4006
|
+
)
|
|
4007
|
+
await cfg.onRecoveryComplete?.({
|
|
4008
|
+
chats: chatsToRecover.length,
|
|
4009
|
+
recovered,
|
|
4010
|
+
durationMs: Date.now() - recoveryStartMs
|
|
4011
|
+
})
|
|
4012
|
+
}
|
|
4013
|
+
function schedulePersist() {
|
|
4014
|
+
if (!cfg.persistPath) return
|
|
4015
|
+
if (persistTimer) {
|
|
4016
|
+
clearTimeout(persistTimer)
|
|
4017
|
+
}
|
|
4018
|
+
persistTimer = setTimeout(() => {
|
|
4019
|
+
void flushPersistence()
|
|
4020
|
+
}, cfg.persistDebounceMs)
|
|
4021
|
+
}
|
|
4022
|
+
async function flushPersistence() {
|
|
4023
|
+
if (!cfg.persistPath) return
|
|
4024
|
+
try {
|
|
4025
|
+
const data = {}
|
|
4026
|
+
for (const [jid, entry] of lastSeen) {
|
|
4027
|
+
data[jid] = {
|
|
4028
|
+
id: entry.messageId,
|
|
4029
|
+
timestamp: entry.timestamp
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
await _fsPromises.writeFile(cfg.persistPath, JSON.stringify(data, null, 2), 'utf-8')
|
|
4033
|
+
} catch (err) {
|
|
4034
|
+
logger.error?.(`[messageRecovery] Failed to persist state: ${err.message}`)
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function loadPersistence() {
|
|
4038
|
+
if (!cfg.persistPath) return
|
|
4039
|
+
try {
|
|
4040
|
+
const fs4 = require('fs')
|
|
4041
|
+
if (!fs4.existsSync(cfg.persistPath)) return
|
|
4042
|
+
const raw = fs4.readFileSync(cfg.persistPath, 'utf-8')
|
|
4043
|
+
const data = JSON.parse(raw)
|
|
4044
|
+
for (const [jid, entry] of Object.entries(data)) {
|
|
4045
|
+
lastSeen.set(jid, {
|
|
4046
|
+
messageId: entry.id,
|
|
4047
|
+
timestamp: entry.timestamp,
|
|
4048
|
+
lastTouchedAt: Date.now()
|
|
4049
|
+
})
|
|
4050
|
+
}
|
|
4051
|
+
logger.info?.(`[messageRecovery] Loaded ${lastSeen.size} entries from ${cfg.persistPath}`)
|
|
4052
|
+
} catch (err) {
|
|
4053
|
+
logger.warn?.(`[messageRecovery] Failed to load persisted state: ${err.message}`)
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return {
|
|
4057
|
+
async stop() {
|
|
4058
|
+
sock.ev.off('connection.update', connectionListener)
|
|
4059
|
+
if (!sock.ev.process) {
|
|
4060
|
+
sock.ev.off('messages.upsert', messagesListener)
|
|
4061
|
+
}
|
|
4062
|
+
if (persistTimer) {
|
|
4063
|
+
clearTimeout(persistTimer)
|
|
4064
|
+
persistTimer = null
|
|
4065
|
+
}
|
|
4066
|
+
await flushPersistence()
|
|
4067
|
+
logger.info?.(`[messageRecovery] Stopped \u2014 total recovered: ${totalRecovered}`)
|
|
4068
|
+
},
|
|
4069
|
+
markSeen(chatJid, messageId, timestamp) {
|
|
4070
|
+
lastSeen.set(chatJid, {
|
|
4071
|
+
messageId,
|
|
4072
|
+
timestamp,
|
|
4073
|
+
lastTouchedAt: Date.now()
|
|
4074
|
+
})
|
|
4075
|
+
schedulePersist()
|
|
4076
|
+
},
|
|
4077
|
+
getStats() {
|
|
4078
|
+
return {
|
|
4079
|
+
trackedChats: lastSeen.size,
|
|
4080
|
+
totalRecovered,
|
|
4081
|
+
lastReconnectAt,
|
|
4082
|
+
lastGapMs
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
// deviceFingerprint.js
|
|
4089
|
+
var DEFAULT_APP_VERSION_POOL = [
|
|
4090
|
+
[2, 24, 5, 18],
|
|
4091
|
+
[2, 24, 5, 17],
|
|
4092
|
+
[2, 24, 4, 77],
|
|
4093
|
+
[2, 24, 5, 15],
|
|
4094
|
+
[2, 24, 3, 91],
|
|
4095
|
+
[2, 24, 5, 20]
|
|
4096
|
+
]
|
|
4097
|
+
var DEFAULT_OS_VERSION_POOL = ['10', '11', '12', '13', '14']
|
|
4098
|
+
var DEFAULT_DEVICE_MODEL_POOL = [
|
|
4099
|
+
'Pixel 6',
|
|
4100
|
+
'Pixel 7',
|
|
4101
|
+
'Galaxy S22',
|
|
4102
|
+
'Galaxy S23',
|
|
4103
|
+
'Xiaomi 13',
|
|
4104
|
+
'Xiaomi 12',
|
|
4105
|
+
'OnePlus 11',
|
|
4106
|
+
'Moto G84',
|
|
4107
|
+
'Moto G54',
|
|
4108
|
+
'Realme 11',
|
|
4109
|
+
'Vivo V29',
|
|
4110
|
+
'Oppo Find X6'
|
|
4111
|
+
]
|
|
4112
|
+
var SeededRandom = class {
|
|
4113
|
+
state
|
|
4114
|
+
constructor(seed) {
|
|
4115
|
+
let hash = 0
|
|
4116
|
+
for (let i = 0; i < seed.length; i++) {
|
|
4117
|
+
hash = (hash << 5) - hash + seed.charCodeAt(i)
|
|
4118
|
+
hash = hash & hash
|
|
4119
|
+
}
|
|
4120
|
+
this.state = Math.abs(hash) || 1
|
|
4121
|
+
}
|
|
4122
|
+
next() {
|
|
4123
|
+
let t = (this.state += 1831565813)
|
|
4124
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
4125
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
4126
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
4127
|
+
}
|
|
4128
|
+
pick(array) {
|
|
4129
|
+
return array[Math.floor(this.next() * array.length)]
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
function generateFingerprint(config = {}, sessionId) {
|
|
4133
|
+
const {
|
|
4134
|
+
enabled = true,
|
|
4135
|
+
randomizeAppVersion = true,
|
|
4136
|
+
randomizeOsVersion = true,
|
|
4137
|
+
randomizeDeviceModel = true,
|
|
4138
|
+
seed,
|
|
4139
|
+
appVersionPool = DEFAULT_APP_VERSION_POOL,
|
|
4140
|
+
osVersionPool = DEFAULT_OS_VERSION_POOL,
|
|
4141
|
+
deviceModelPool = DEFAULT_DEVICE_MODEL_POOL
|
|
4142
|
+
} = config
|
|
4143
|
+
const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`
|
|
4144
|
+
const rng = new SeededRandom(seed || finalSessionId)
|
|
4145
|
+
const appVersion = enabled && randomizeAppVersion ? rng.pick(appVersionPool) : appVersionPool[0]
|
|
4146
|
+
const osVersion = enabled && randomizeOsVersion ? rng.pick(osVersionPool) : osVersionPool[0]
|
|
4147
|
+
const deviceModel = enabled && randomizeDeviceModel ? rng.pick(deviceModelPool) : deviceModelPool[0]
|
|
4148
|
+
return {
|
|
4149
|
+
appVersion: [...appVersion],
|
|
4150
|
+
// Copy to avoid mutation
|
|
4151
|
+
osVersion,
|
|
4152
|
+
deviceModel,
|
|
4153
|
+
sessionId: finalSessionId
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
function applyFingerprint(socketConfig, fp) {
|
|
4157
|
+
const config = { ...socketConfig }
|
|
4158
|
+
if (config.version !== void 0 || 'version' in config || true) {
|
|
4159
|
+
config.version = fp.appVersion
|
|
4160
|
+
}
|
|
4161
|
+
if (config.browser !== void 0 || 'browser' in config || true) {
|
|
4162
|
+
config.browser = [fp.deviceModel, fp.osVersion, `WhatsApp/${fp.appVersion.join('.')}`]
|
|
4163
|
+
}
|
|
4164
|
+
return config
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// credsSnapshot.js
|
|
4168
|
+
var import_fs = require('fs')
|
|
4169
|
+
var path2 = __toESM(require('path'), 1)
|
|
4170
|
+
var noop = () => {}
|
|
4171
|
+
function credsSnapshot(config) {
|
|
4172
|
+
const { credsPath, snapshotDir = path2.join(path2.dirname(credsPath), '.snapshots'), keep = 3, logger = {} } = config
|
|
4173
|
+
const log = {
|
|
4174
|
+
info: logger.info || noop,
|
|
4175
|
+
warn: logger.warn || noop,
|
|
4176
|
+
error: logger.error || noop
|
|
4177
|
+
}
|
|
4178
|
+
async function take() {
|
|
4179
|
+
try {
|
|
4180
|
+
try {
|
|
4181
|
+
await import_fs.promises.access(credsPath)
|
|
4182
|
+
} catch {
|
|
4183
|
+
log.warn(`[credsSnapshot] Creds file not found: ${credsPath}`)
|
|
4184
|
+
return null
|
|
4185
|
+
}
|
|
4186
|
+
await import_fs.promises.mkdir(snapshotDir, { recursive: true })
|
|
4187
|
+
const timestamp = /* @__PURE__ */ new Date().toISOString().replace(/[:.]/g, '-')
|
|
4188
|
+
const snapshotPath = path2.join(snapshotDir, `creds-${timestamp}.json`)
|
|
4189
|
+
const tmpPath = `${snapshotPath}.tmp`
|
|
4190
|
+
await import_fs.promises.copyFile(credsPath, tmpPath)
|
|
4191
|
+
await import_fs.promises.rename(tmpPath, snapshotPath)
|
|
4192
|
+
log.info(`[credsSnapshot] Snapshot taken: ${snapshotPath}`)
|
|
4193
|
+
await rotate()
|
|
4194
|
+
return snapshotPath
|
|
4195
|
+
} catch (err) {
|
|
4196
|
+
log.error(`[credsSnapshot] Failed to take snapshot: ${err}`)
|
|
4197
|
+
return null
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
async function rotate() {
|
|
4201
|
+
try {
|
|
4202
|
+
const snapshots = await list()
|
|
4203
|
+
const toDelete = snapshots.slice(keep)
|
|
4204
|
+
for (const snap of toDelete) {
|
|
4205
|
+
await import_fs.promises.unlink(snap.path)
|
|
4206
|
+
log.info(`[credsSnapshot] Rotated out: ${snap.path}`)
|
|
4207
|
+
}
|
|
4208
|
+
} catch (err) {
|
|
4209
|
+
log.error(`[credsSnapshot] Rotation failed: ${err}`)
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
async function list() {
|
|
4213
|
+
try {
|
|
4214
|
+
await import_fs.promises.access(snapshotDir)
|
|
4215
|
+
} catch {
|
|
4216
|
+
return []
|
|
4217
|
+
}
|
|
4218
|
+
try {
|
|
4219
|
+
const files = await import_fs.promises.readdir(snapshotDir)
|
|
4220
|
+
const snapshots = await Promise.all(
|
|
4221
|
+
files
|
|
4222
|
+
.filter(f => f.startsWith('creds-') && f.endsWith('.json'))
|
|
4223
|
+
.map(async f => {
|
|
4224
|
+
const fullPath = path2.join(snapshotDir, f)
|
|
4225
|
+
const stat = await import_fs.promises.stat(fullPath)
|
|
4226
|
+
return {
|
|
4227
|
+
path: fullPath,
|
|
4228
|
+
takenAt: stat.mtime,
|
|
4229
|
+
size: stat.size
|
|
4230
|
+
}
|
|
4231
|
+
})
|
|
4232
|
+
)
|
|
4233
|
+
return snapshots.sort((a, b) => b.takenAt.getTime() - a.takenAt.getTime())
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
log.error(`[credsSnapshot] Failed to list snapshots: ${err}`)
|
|
4236
|
+
return []
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
async function restoreLatest() {
|
|
4240
|
+
const snapshots = await list()
|
|
4241
|
+
if (snapshots.length === 0) {
|
|
4242
|
+
log.warn('[credsSnapshot] No snapshots available to restore')
|
|
4243
|
+
return false
|
|
4244
|
+
}
|
|
4245
|
+
return restore(snapshots[0].path)
|
|
4246
|
+
}
|
|
4247
|
+
async function restore(snapshotPath) {
|
|
4248
|
+
try {
|
|
4249
|
+
await import_fs.promises.access(snapshotPath)
|
|
4250
|
+
const tmpPath = `${credsPath}.tmp`
|
|
4251
|
+
await import_fs.promises.copyFile(snapshotPath, tmpPath)
|
|
4252
|
+
await import_fs.promises.rename(tmpPath, credsPath)
|
|
4253
|
+
log.info(`[credsSnapshot] Restored from: ${snapshotPath}`)
|
|
4254
|
+
return true
|
|
4255
|
+
} catch (err) {
|
|
4256
|
+
log.error(`[credsSnapshot] Failed to restore from ${snapshotPath}: ${err}`)
|
|
4257
|
+
return false
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
return {
|
|
4261
|
+
take,
|
|
4262
|
+
restoreLatest,
|
|
4263
|
+
restore,
|
|
4264
|
+
list
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
// readReceiptVariance.js
|
|
4269
|
+
function gaussianRandom() {
|
|
4270
|
+
let u = 0
|
|
4271
|
+
let v = 0
|
|
4272
|
+
while (u === 0) u = Math.random()
|
|
4273
|
+
while (v === 0) v = Math.random()
|
|
4274
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
|
|
4275
|
+
}
|
|
4276
|
+
function readReceiptVariance(config = {}) {
|
|
4277
|
+
const { meanMs = 1500, stdDevMs = 800, minMs = 200, maxMs = 8e3, skipIfOlderThanMs = 6e4 } = config
|
|
4278
|
+
const pendingTimers = /* @__PURE__ */ new Set()
|
|
4279
|
+
function delayMs() {
|
|
4280
|
+
const gaussian = gaussianRandom()
|
|
4281
|
+
const value = meanMs + gaussian * stdDevMs
|
|
4282
|
+
return Math.max(minMs, Math.min(maxMs, value))
|
|
4283
|
+
}
|
|
4284
|
+
function wrap(sock) {
|
|
4285
|
+
const originalReadMessages = sock.readMessages.bind(sock)
|
|
4286
|
+
const wrappedReadMessages = async keys => {
|
|
4287
|
+
const now = Date.now()
|
|
4288
|
+
const oldMessages = keys.every(key => {
|
|
4289
|
+
if (!key.messageTimestamp) return false
|
|
4290
|
+
const msgTime =
|
|
4291
|
+
typeof key.messageTimestamp === 'number'
|
|
4292
|
+
? key.messageTimestamp * 1e3
|
|
4293
|
+
: parseInt(key.messageTimestamp, 10) * 1e3
|
|
4294
|
+
return now - msgTime > skipIfOlderThanMs
|
|
4295
|
+
})
|
|
4296
|
+
if (oldMessages) {
|
|
4297
|
+
return originalReadMessages(keys)
|
|
4298
|
+
}
|
|
4299
|
+
const delay = delayMs()
|
|
4300
|
+
return new Promise((resolve, reject) => {
|
|
4301
|
+
const timer = setTimeout(async () => {
|
|
4302
|
+
pendingTimers.delete(timer)
|
|
4303
|
+
try {
|
|
4304
|
+
const result = await originalReadMessages(keys)
|
|
4305
|
+
resolve(result)
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
reject(err)
|
|
4308
|
+
}
|
|
4309
|
+
}, delay)
|
|
4310
|
+
pendingTimers.add(timer)
|
|
4311
|
+
})
|
|
4312
|
+
}
|
|
4313
|
+
return new Proxy(sock, {
|
|
4314
|
+
get(target, prop) {
|
|
4315
|
+
if (prop === 'readMessages') {
|
|
4316
|
+
return wrappedReadMessages
|
|
4317
|
+
}
|
|
4318
|
+
return target[prop]
|
|
4319
|
+
}
|
|
4320
|
+
})
|
|
4321
|
+
}
|
|
4322
|
+
function stop() {
|
|
4323
|
+
for (const timer of pendingTimers) {
|
|
4324
|
+
clearTimeout(timer)
|
|
4325
|
+
}
|
|
4326
|
+
pendingTimers.clear()
|
|
4327
|
+
}
|
|
4328
|
+
return {
|
|
4329
|
+
wrap,
|
|
4330
|
+
delayMs,
|
|
4331
|
+
stop
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
// proxyRotator.js
|
|
4336
|
+
var import_node_module = require('node:module')
|
|
4337
|
+
var require2 = (0, import_node_module.createRequire)(__importMetaUrl)
|
|
4338
|
+
var NoopLogger = {
|
|
4339
|
+
info: () => {},
|
|
4340
|
+
warn: () => {},
|
|
4341
|
+
error: () => {}
|
|
4342
|
+
}
|
|
4343
|
+
function proxyRotator(config) {
|
|
4344
|
+
const {
|
|
4345
|
+
pool,
|
|
4346
|
+
strategy = 'round-robin',
|
|
4347
|
+
rotateOn = ['disconnect', 'ban-warning'],
|
|
4348
|
+
scheduledIntervalMs = 0,
|
|
4349
|
+
maxFailures = 3,
|
|
4350
|
+
deadCooldownMs = 6e5,
|
|
4351
|
+
// 10 minutes
|
|
4352
|
+
logger = NoopLogger
|
|
4353
|
+
} = config
|
|
4354
|
+
if (!pool || pool.length === 0) {
|
|
4355
|
+
throw new Error('proxyRotator: pool cannot be empty')
|
|
4356
|
+
}
|
|
4357
|
+
if (pool.length === 1) {
|
|
4358
|
+
logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.')
|
|
4359
|
+
}
|
|
4360
|
+
if (scheduledIntervalMs > 0 && scheduledIntervalMs < 6e4) {
|
|
4361
|
+
logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`)
|
|
4362
|
+
}
|
|
4363
|
+
const states = pool.map(endpoint => ({
|
|
4364
|
+
endpoint,
|
|
4365
|
+
failures: 0,
|
|
4366
|
+
lastUsedAt: null,
|
|
4367
|
+
isDead: false
|
|
4368
|
+
}))
|
|
4369
|
+
let currentIndex = 0
|
|
4370
|
+
let totalRotations = 0
|
|
4371
|
+
const rotationsByTrigger = {}
|
|
4372
|
+
let scheduledTimer = null
|
|
4373
|
+
const agentCache = /* @__PURE__ */ new Map()
|
|
4374
|
+
const moduleCache = {}
|
|
4375
|
+
function buildProxyUrl(endpoint) {
|
|
4376
|
+
const { type, host, port, username, password } = endpoint
|
|
4377
|
+
const auth = username && password ? `${username}:${password}@` : ''
|
|
4378
|
+
return `${type}://${auth}${host}:${port}`
|
|
4379
|
+
}
|
|
4380
|
+
function createAgentForEndpointSync(endpoint) {
|
|
4381
|
+
if (agentCache.has(endpoint)) {
|
|
4382
|
+
return agentCache.get(endpoint)
|
|
4383
|
+
}
|
|
4384
|
+
const url = buildProxyUrl(endpoint)
|
|
4385
|
+
let agent = null
|
|
4386
|
+
try {
|
|
4387
|
+
if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
|
|
4388
|
+
if (!moduleCache['socks-proxy-agent']) {
|
|
4389
|
+
try {
|
|
4390
|
+
moduleCache['socks-proxy-agent'] = require2('socks-proxy-agent')
|
|
4391
|
+
} catch {
|
|
4392
|
+
logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent')
|
|
4393
|
+
return null
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url)
|
|
4397
|
+
} else if (endpoint.type === 'http') {
|
|
4398
|
+
if (!moduleCache['http-proxy-agent']) {
|
|
4399
|
+
try {
|
|
4400
|
+
moduleCache['http-proxy-agent'] = require2('http-proxy-agent')
|
|
4401
|
+
} catch {
|
|
4402
|
+
logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent')
|
|
4403
|
+
return null
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url)
|
|
4407
|
+
} else if (endpoint.type === 'https') {
|
|
4408
|
+
if (!moduleCache['https-proxy-agent']) {
|
|
4409
|
+
try {
|
|
4410
|
+
moduleCache['https-proxy-agent'] = require2('https-proxy-agent')
|
|
4411
|
+
} catch {
|
|
4412
|
+
logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent')
|
|
4413
|
+
return null
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url)
|
|
4417
|
+
} else {
|
|
4418
|
+
logger.error?.(`Unknown proxy type: ${endpoint.type}`)
|
|
4419
|
+
return null
|
|
4420
|
+
}
|
|
4421
|
+
if (agent) {
|
|
4422
|
+
agentCache.set(endpoint, agent)
|
|
4423
|
+
}
|
|
4424
|
+
return agent
|
|
4425
|
+
} catch (err) {
|
|
4426
|
+
logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`)
|
|
4427
|
+
return null
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
function getAliveEndpoints() {
|
|
4431
|
+
const now = Date.now()
|
|
4432
|
+
return states
|
|
4433
|
+
.map((s, idx) => {
|
|
4434
|
+
if (s.isDead && s.lastUsedAt) {
|
|
4435
|
+
if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
|
|
4436
|
+
s.isDead = false
|
|
4437
|
+
s.failures = 0
|
|
4438
|
+
logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`)
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
const cooldown = s.endpoint.cooldownMs || 0
|
|
4442
|
+
if (cooldown > 0 && s.lastUsedAt) {
|
|
4443
|
+
if (now - s.lastUsedAt.getTime() < cooldown) {
|
|
4444
|
+
return -1
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
return !s.isDead ? idx : -1
|
|
4448
|
+
})
|
|
4449
|
+
.filter(idx => idx !== -1)
|
|
4450
|
+
}
|
|
4451
|
+
function selectNextIndex(alive) {
|
|
4452
|
+
if (alive.length === 0) return currentIndex
|
|
4453
|
+
if (strategy === 'round-robin') {
|
|
4454
|
+
const afterCurrent = alive.filter(idx => idx > currentIndex)
|
|
4455
|
+
if (afterCurrent.length > 0) return afterCurrent[0]
|
|
4456
|
+
return alive[0]
|
|
4457
|
+
}
|
|
4458
|
+
if (strategy === 'random') {
|
|
4459
|
+
return alive[Math.floor(Math.random() * alive.length)]
|
|
4460
|
+
}
|
|
4461
|
+
if (strategy === 'least-recently-used') {
|
|
4462
|
+
const neverUsed = alive.filter(idx => states[idx].lastUsedAt === null)
|
|
4463
|
+
if (neverUsed.length > 0) {
|
|
4464
|
+
return neverUsed[0]
|
|
4465
|
+
}
|
|
4466
|
+
let oldestIdx = alive[0]
|
|
4467
|
+
let oldestTime = states[oldestIdx].lastUsedAt.getTime()
|
|
4468
|
+
for (const idx of alive) {
|
|
4469
|
+
const time = states[idx].lastUsedAt.getTime()
|
|
4470
|
+
if (time < oldestTime) {
|
|
4471
|
+
oldestTime = time
|
|
4472
|
+
oldestIdx = idx
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
return oldestIdx
|
|
4476
|
+
}
|
|
4477
|
+
if (strategy === 'weighted') {
|
|
4478
|
+
const weights = alive.map(idx => {
|
|
4479
|
+
const failures = states[idx].failures
|
|
4480
|
+
return 1 / (failures + 1)
|
|
4481
|
+
})
|
|
4482
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0)
|
|
4483
|
+
let rand = Math.random() * totalWeight
|
|
4484
|
+
for (let i = 0; i < alive.length; i++) {
|
|
4485
|
+
rand -= weights[i]
|
|
4486
|
+
if (rand <= 0) return alive[i]
|
|
4487
|
+
}
|
|
4488
|
+
return alive[alive.length - 1]
|
|
4489
|
+
}
|
|
4490
|
+
return alive[0]
|
|
4491
|
+
}
|
|
4492
|
+
function rotateImpl(reason = 'manual') {
|
|
4493
|
+
if (pool.length === 1) {
|
|
4494
|
+
return states[0].endpoint
|
|
4495
|
+
}
|
|
4496
|
+
const alive = getAliveEndpoints()
|
|
4497
|
+
if (alive.length === 0) {
|
|
4498
|
+
logger.warn?.('All endpoints are dead. Cannot rotate.')
|
|
4499
|
+
return states[currentIndex].endpoint
|
|
4500
|
+
}
|
|
4501
|
+
const nextIdx = selectNextIndex(alive)
|
|
4502
|
+
if (nextIdx === currentIndex && alive.length > 1) {
|
|
4503
|
+
const others = alive.filter(idx => idx !== currentIndex)
|
|
4504
|
+
if (others.length > 0) {
|
|
4505
|
+
currentIndex = others[0]
|
|
4506
|
+
} else {
|
|
4507
|
+
currentIndex = nextIdx
|
|
4508
|
+
}
|
|
4509
|
+
} else {
|
|
4510
|
+
currentIndex = nextIdx
|
|
4511
|
+
}
|
|
4512
|
+
states[currentIndex].lastUsedAt = /* @__PURE__ */ new Date()
|
|
4513
|
+
totalRotations++
|
|
4514
|
+
rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1
|
|
4515
|
+
const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host
|
|
4516
|
+
logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`)
|
|
4517
|
+
return states[currentIndex].endpoint
|
|
4518
|
+
}
|
|
4519
|
+
function markFailureImpl() {
|
|
4520
|
+
const state = states[currentIndex]
|
|
4521
|
+
state.failures++
|
|
4522
|
+
const label = state.endpoint.label || state.endpoint.host
|
|
4523
|
+
logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`)
|
|
4524
|
+
if (state.failures >= maxFailures) {
|
|
4525
|
+
state.isDead = true
|
|
4526
|
+
logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`)
|
|
4527
|
+
const alive = getAliveEndpoints()
|
|
4528
|
+
if (alive.length > 0) {
|
|
4529
|
+
rotateImpl('manual')
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
function resurrectAllImpl() {
|
|
4534
|
+
let count = 0
|
|
4535
|
+
for (const state of states) {
|
|
4536
|
+
if (state.isDead) {
|
|
4537
|
+
state.isDead = false
|
|
4538
|
+
state.failures = 0
|
|
4539
|
+
count++
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
if (count > 0) {
|
|
4543
|
+
logger.info?.(`Resurrected ${count} dead endpoint(s)`)
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
function stopImpl() {
|
|
4547
|
+
if (scheduledTimer) {
|
|
4548
|
+
clearInterval(scheduledTimer)
|
|
4549
|
+
scheduledTimer = null
|
|
4550
|
+
logger.info?.('Stopped scheduled rotation timer')
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
function getStatsImpl() {
|
|
4554
|
+
return {
|
|
4555
|
+
totalRotations,
|
|
4556
|
+
rotationsByTrigger: { ...rotationsByTrigger },
|
|
4557
|
+
endpointHealth: states.map(s => ({
|
|
4558
|
+
label: s.endpoint.label || s.endpoint.host,
|
|
4559
|
+
inUse: states[currentIndex] === s,
|
|
4560
|
+
failures: s.failures,
|
|
4561
|
+
lastUsedAt: s.lastUsedAt,
|
|
4562
|
+
isDead: s.isDead
|
|
4563
|
+
})),
|
|
4564
|
+
currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
function currentAgentImpl() {
|
|
4568
|
+
const endpoint = states[currentIndex].endpoint
|
|
4569
|
+
return createAgentForEndpointSync(endpoint)
|
|
4570
|
+
}
|
|
4571
|
+
function currentImpl() {
|
|
4572
|
+
return states[currentIndex].endpoint
|
|
4573
|
+
}
|
|
4574
|
+
if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
|
|
4575
|
+
scheduledTimer = setInterval(() => {
|
|
4576
|
+
rotateImpl('scheduled')
|
|
4577
|
+
}, scheduledIntervalMs)
|
|
4578
|
+
logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`)
|
|
4579
|
+
}
|
|
4580
|
+
states[0].lastUsedAt = /* @__PURE__ */ new Date()
|
|
4581
|
+
return {
|
|
4582
|
+
currentAgent: currentAgentImpl,
|
|
4583
|
+
current: currentImpl,
|
|
4584
|
+
rotate: rotateImpl,
|
|
4585
|
+
markFailure: markFailureImpl,
|
|
4586
|
+
resurrectAll: resurrectAllImpl,
|
|
4587
|
+
stop: stopImpl,
|
|
4588
|
+
getStats: getStatsImpl
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4592
|
+
0 &&
|
|
4593
|
+
(module.exports = {
|
|
4594
|
+
AntiBan,
|
|
4595
|
+
ContactGraphWarmer,
|
|
4596
|
+
ContentVariator,
|
|
4597
|
+
FileStateAdapter,
|
|
4598
|
+
HealthMonitor,
|
|
4599
|
+
JidCanonicalizer,
|
|
4600
|
+
LidFirstResolver,
|
|
4601
|
+
LidResolver,
|
|
4602
|
+
MAC_ERROR_CODES,
|
|
4603
|
+
MessageQueue,
|
|
4604
|
+
MessageRetryReason,
|
|
4605
|
+
PRESETS,
|
|
4606
|
+
PostReconnectThrottle,
|
|
4607
|
+
PresenceChoreographer,
|
|
4608
|
+
RateLimiter,
|
|
4609
|
+
ReplyRatioGuard,
|
|
4610
|
+
RetryReasonTracker,
|
|
4611
|
+
Scheduler,
|
|
4612
|
+
SessionHealthMonitor,
|
|
4613
|
+
StateManager,
|
|
4614
|
+
TimelockGuard,
|
|
4615
|
+
WarmUp,
|
|
4616
|
+
WebhookAlerts,
|
|
4617
|
+
applyFingerprint,
|
|
4618
|
+
applyGroupMultiplier,
|
|
4619
|
+
classifyDisconnect,
|
|
4620
|
+
createLidFirstResolver,
|
|
4621
|
+
credsSnapshot,
|
|
4622
|
+
generateFingerprint,
|
|
4623
|
+
getCircadianMultiplier,
|
|
4624
|
+
getRetryReasonDescription,
|
|
4625
|
+
isBroadcast,
|
|
4626
|
+
isGroup,
|
|
4627
|
+
isMacError,
|
|
4628
|
+
isNewsletter,
|
|
4629
|
+
messageRecovery,
|
|
4630
|
+
parseRetryReason,
|
|
4631
|
+
proxyRotator,
|
|
4632
|
+
readReceiptVariance,
|
|
4633
|
+
resolveConfig,
|
|
4634
|
+
shouldUseGroupProfile,
|
|
4635
|
+
wrapSocket,
|
|
4636
|
+
wrapWithSessionStability
|
|
4637
|
+
})
|