@eryxenx/fca 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/index.js +7 -0
- package/module/login.js +5 -1
- package/module/options.js +4 -0
- package/package.json +2 -2
- package/src/api/socket/listenE2EE.js +17 -0
- package/src/api/socket/listenMqtt.js +32 -0
- package/src/utils/humanizer.js +199 -0
package/README.md
CHANGED
|
@@ -420,6 +420,109 @@ api.uploadImageToImgbb(imageUrl);
|
|
|
420
420
|
|
|
421
421
|
---
|
|
422
422
|
|
|
423
|
+
## 🛡️ Command Anti-Spam (Humanizer)
|
|
424
|
+
|
|
425
|
+
Nobody — not even the bot owner — can spam the bot with commands. This makes
|
|
426
|
+
the account behave like a human operator, which reduces the chance of
|
|
427
|
+
Facebook flagging it as a script and logging it out / suspending it.
|
|
428
|
+
|
|
429
|
+
On by default. It works on **every incoming command** (and every incoming
|
|
430
|
+
message that starts with a command prefix):
|
|
431
|
+
|
|
432
|
+
- **Human reaction delay** — an accepted command is only delivered to your
|
|
433
|
+
listener after a short random delay (default `2–3s`), so the bot replies
|
|
434
|
+
like a human typing, never instantly like a script.
|
|
435
|
+
- **Spam window** — after a command is accepted in a conversation, any further
|
|
436
|
+
commands in that conversation within `cooldownMs` (default `5.5s`) are
|
|
437
|
+
**silently ignored**. `help help help help` → exactly **one** reply.
|
|
438
|
+
- **Idle reset** — after `resetMs` (default `10s`) with no commands in a
|
|
439
|
+
conversation, the state resets and the next command starts fresh.
|
|
440
|
+
- **Global cap** — at most `maxPerMinute` commands are accepted per minute
|
|
441
|
+
across all conversations (default `20`).
|
|
442
|
+
|
|
443
|
+
Non-command messages always pass through instantly, untouched.
|
|
444
|
+
|
|
445
|
+
### How to use
|
|
446
|
+
|
|
447
|
+
It's already active after `login()`. No code needed:
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
const login = require("@eryxenx/fca");
|
|
451
|
+
|
|
452
|
+
login({ appState }, {}, (err, api) => {
|
|
453
|
+
if (err) throw err;
|
|
454
|
+
|
|
455
|
+
api.listenMqtt((err, event) => {
|
|
456
|
+
if (err) throw err;
|
|
457
|
+
// Commands arrive here already delayed by 2-3s, and spam bursts are gone.
|
|
458
|
+
if (event.body === "/help") api.sendMessage("help text", event.threadID);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Configuration
|
|
464
|
+
|
|
465
|
+
Pass a `humanize` object to `login()`, or call `api.setOptions({ humanize })`
|
|
466
|
+
at runtime:
|
|
467
|
+
|
|
468
|
+
| Option | Type | Default | Description |
|
|
469
|
+
|--------|------|---------|-------------|
|
|
470
|
+
| `enabled` | boolean | `true` | Master switch |
|
|
471
|
+
| `reactDelayMs` | `[min, max]` | `[2000, 3000]` | Random delay before an accepted command is delivered |
|
|
472
|
+
| `cooldownMs` | number | `5500` | Spam window per conversation (seconds between commands) |
|
|
473
|
+
| `resetMs` | number | `10000` | Idle time after which a conversation resets |
|
|
474
|
+
| `maxPerMinute` | number | `20` | Global command cap per minute (0 = unlimited) |
|
|
475
|
+
| `commands` | string[] | `["/", "!", "."]` | Prefixes that mark a message as a command |
|
|
476
|
+
| `perThread` | boolean | `true` | Cooldown per conversation (false = global) |
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
const login = require("@eryxenx/fca");
|
|
480
|
+
|
|
481
|
+
login({ appState }, {
|
|
482
|
+
humanize: {
|
|
483
|
+
reactDelayMs: [2500, 4000],
|
|
484
|
+
cooldownMs: 6000,
|
|
485
|
+
resetMs: 12000,
|
|
486
|
+
maxPerMinute: 15,
|
|
487
|
+
commands: ["/"]
|
|
488
|
+
}
|
|
489
|
+
}, (err, api) => { /* ... */ });
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
To turn it off completely:
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
// option A: at login
|
|
496
|
+
login({ appState }, { humanize: { enabled: false } }, cb);
|
|
497
|
+
// option B: at runtime
|
|
498
|
+
api.setOptions({ humanize: { enabled: false } });
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Programmatic access
|
|
502
|
+
|
|
503
|
+
The humanizer is a normal class, also exported from the package:
|
|
504
|
+
|
|
505
|
+
```javascript
|
|
506
|
+
const { CommandHumanizer, createCommandHumanizer } = require("@eryxenx/fca");
|
|
507
|
+
|
|
508
|
+
const h = new CommandHumanizer({ reactDelayMs: [2000, 3000] });
|
|
509
|
+
const d = h.shouldProcess({ body: "/help", threadID: "t1" });
|
|
510
|
+
// d.ok === true, d.delayMs in [2000, 3000]
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
At runtime, `api.getHumanizer()` returns the active instance so you can
|
|
514
|
+
inspect statistics or reset a conversation:
|
|
515
|
+
|
|
516
|
+
```javascript
|
|
517
|
+
const h = api.getHumanizer();
|
|
518
|
+
console.log(h.stats()); // accepted / dropped counts
|
|
519
|
+
h.reset("381234567890"); // clear the state for one thread
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
> Note: this operates on **incoming** command volume. Outgoing send pacing
|
|
523
|
+
> (the `globalAntiSuspension` pacer in `src/utils/antiSuspension.js`) is a
|
|
524
|
+
> separate, complementary mechanism that spaces out actual `sendMessage` calls.
|
|
525
|
+
|
|
423
526
|
## 📋 Login Options
|
|
424
527
|
|
|
425
528
|
| Option | Type | Default | Description |
|
|
@@ -435,6 +538,7 @@ api.uploadImageToImgbb(imageUrl);
|
|
|
435
538
|
| `emitReady` | boolean | false | Emit ready event when MQTT connected |
|
|
436
539
|
| `proxy` | string | — | HTTP proxy URL |
|
|
437
540
|
| `userAgent` | string | Safari UA | Override HTTP User-Agent |
|
|
541
|
+
| `humanize` | object | enabled | Command anti-spam config (see above) |
|
|
438
542
|
|
|
439
543
|
---
|
|
440
544
|
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { createFcaClient } = require("./src/app/createFcaClient");
|
|
|
5
5
|
const { MessengerBot, createMessengerBot } = require("./src/app/MessengerBot");
|
|
6
6
|
const { MessengerContext } = require("./src/app/MessengerContext");
|
|
7
7
|
const { attachThreadInfoRealtimeSync, applyThreadInfoRealtimeEvent } = require("./src/app/threadInfoRealtimeSync");
|
|
8
|
+
const humanizer = require("./src/utils/humanizer");
|
|
8
9
|
|
|
9
10
|
// CommonJS default export — the login function (classic FCA / GoatBot compatible)
|
|
10
11
|
module.exports = login;
|
|
@@ -20,3 +21,9 @@ module.exports.createMessengerBot = createMessengerBot;
|
|
|
20
21
|
module.exports.MessengerContext = MessengerContext;
|
|
21
22
|
module.exports.attachThreadInfoRealtimeSync = attachThreadInfoRealtimeSync;
|
|
22
23
|
module.exports.applyThreadInfoRealtimeEvent = applyThreadInfoRealtimeEvent;
|
|
24
|
+
|
|
25
|
+
// Command anti-spam / humanizer
|
|
26
|
+
module.exports.CommandHumanizer = humanizer.CommandHumanizer;
|
|
27
|
+
module.exports.createCommandHumanizer = humanizer.createCommandHumanizer;
|
|
28
|
+
module.exports.createHumanizerMiddleware = humanizer.createHumanizerMiddleware;
|
|
29
|
+
module.exports.humanizer = humanizer;
|
package/module/login.js
CHANGED
|
@@ -98,8 +98,12 @@ function login(loginData, options, callback) {
|
|
|
98
98
|
autoReconnect: true,
|
|
99
99
|
online: true,
|
|
100
100
|
emitReady: false,
|
|
101
|
-
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
|
|
101
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
102
|
+
humanize: undefined
|
|
102
103
|
};
|
|
104
|
+
if (options && options.humanize !== undefined) {
|
|
105
|
+
globalOptions.humanize = options.humanize;
|
|
106
|
+
}
|
|
103
107
|
setOptions(globalOptions, options);
|
|
104
108
|
let prCallback = null;
|
|
105
109
|
let rejectFunc = null;
|
package/module/options.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eryxenx/fca",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Facebook Chat API by EryXenX | Stable • Auto Re-login • Full E2EE Support —
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Facebook Chat API by EryXenX | Stable • Auto Re-login • Full E2EE Support — with Command Anti-Spam (Humanizer)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var logger = require("../../utils/nexca-logger");
|
|
4
4
|
var EventEmitter = require("events");
|
|
5
|
+
var { createHumanizerMiddleware } = require("../../utils/humanizer");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* listenE2EE — merges E2EE messages into the same event stream as MQTT.
|
|
@@ -64,6 +65,22 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
64
65
|
if (!ctx.globalOptions.selfListen && event.senderID === ctx.userID) return;
|
|
65
66
|
|
|
66
67
|
event.isE2EE = true;
|
|
68
|
+
|
|
69
|
+
// Run E2EE events through the same middleware pipeline as
|
|
70
|
+
// MQTT events so command anti-spam also applies here.
|
|
71
|
+
if (ctx._middleware && ctx._middleware.count > 0) {
|
|
72
|
+
// Ensure the humanizer middleware is present for E2EE too.
|
|
73
|
+
if (ctx._humanizer && !ctx._humanizerMiddlewareInstalled) {
|
|
74
|
+
ctx._middleware.use("humanizer", createHumanizerMiddleware(ctx._humanizer, logger));
|
|
75
|
+
ctx._humanizerMiddlewareInstalled = true;
|
|
76
|
+
}
|
|
77
|
+
return ctx._middleware.process(event, (err2, processed) => {
|
|
78
|
+
if (err2) { logger.error("listenE2EE", "Middleware error: " + (err2 && err2.message ? err2.message : String(err2))); return; }
|
|
79
|
+
if (!processed) return; // dropped by middleware
|
|
80
|
+
globalCallback(null, processed);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
globalCallback(null, event);
|
|
68
85
|
});
|
|
69
86
|
|
|
@@ -17,6 +17,7 @@ const createGetSeqID = require("./core/getSeqID");
|
|
|
17
17
|
const getTaskResponseData = require("./core/getTaskResponseData");
|
|
18
18
|
const createEmitAuth = require("./core/emitAuth");
|
|
19
19
|
const createMiddlewareSystem = require("./middleware");
|
|
20
|
+
const { createCommandHumanizer, createHumanizerMiddleware } = require("../../utils/humanizer");
|
|
20
21
|
|
|
21
22
|
// Auto-cycle is OFF by default now: a real browser keeps one MQTT socket
|
|
22
23
|
// open for hours. Cycling every exactly-3600.000s (full unsubscribe,
|
|
@@ -50,6 +51,18 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
50
51
|
}
|
|
51
52
|
const middleware = ctx._middleware;
|
|
52
53
|
|
|
54
|
+
function installHumanizerMiddleware() {
|
|
55
|
+
if (ctx._humanizerMiddlewareInstalled) return;
|
|
56
|
+
ctx._humanizerMiddlewareInstalled = true;
|
|
57
|
+
|
|
58
|
+
if (!ctx._humanizer) {
|
|
59
|
+
ctx._humanizer = createCommandHumanizer(ctx.globalOptions.humanize);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
middleware.use("humanizer", createHumanizerMiddleware(ctx._humanizer, logger));
|
|
63
|
+
logger("Humanizer middleware installed (command anti-spam active)", "info");
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
function installPostGuard() {
|
|
54
67
|
if (ctx._postGuarded) return defaultFuncs.post;
|
|
55
68
|
const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
|
|
@@ -349,6 +362,25 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
349
362
|
|
|
350
363
|
const msgEmitter = new MessageEmitter();
|
|
351
364
|
|
|
365
|
+
// Command anti-spam. Enabled by default so that nobody (the owner or
|
|
366
|
+
// anyone else) can spam the bot with commands — which also protects the
|
|
367
|
+
// account from looking automated to Facebook. Opt out via
|
|
368
|
+
// api.setOptions({ humanize: { enabled: false } }) or pass
|
|
369
|
+
// { humanize: { enabled: false } } to login().
|
|
370
|
+
const humanizeEnabled = ctx.globalOptions.humanize === undefined ||
|
|
371
|
+
(ctx.globalOptions.humanize && ctx.globalOptions.humanize.enabled !== false);
|
|
372
|
+
|
|
373
|
+
if (humanizeEnabled) {
|
|
374
|
+
try {
|
|
375
|
+
installHumanizerMiddleware();
|
|
376
|
+
} catch (e) {
|
|
377
|
+
logger(`Humanizer middleware install failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Expose the humanizer for diagnostics / runtime reconfiguration.
|
|
382
|
+
api.getHumanizer = () => ctx._humanizer || null;
|
|
383
|
+
|
|
352
384
|
// Original callback without middleware
|
|
353
385
|
const originalCallback = callback || function (error, message) {
|
|
354
386
|
if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CommandHumanizer — makes a bot behave like a human when it receives
|
|
5
|
+
* commands so that neither the owner nor anyone else can spam the bot.
|
|
6
|
+
* This keeps the account from looking like a script to Facebook.
|
|
7
|
+
*
|
|
8
|
+
* Behavior (all tunable via options):
|
|
9
|
+
* 1. Human reaction delay — an accepted command is only forwarded to the
|
|
10
|
+
* bot handler after a short random delay (default 2-3 seconds), so the
|
|
11
|
+
* reply never fires instantly like a script.
|
|
12
|
+
* 2. Spam window — once a command in a conversation is accepted, any
|
|
13
|
+
* further command in that same conversation within `cooldownMs`
|
|
14
|
+
* (default 5.5s) is IGNORED. A burst such as "help help help help"
|
|
15
|
+
* therefore gets exactly one reply.
|
|
16
|
+
* 3. Idle reset — after `resetMs` (default 10s) with no commands in a
|
|
17
|
+
* conversation, the state resets and the next command is treated as a
|
|
18
|
+
* fresh one.
|
|
19
|
+
* 4. Global cap — at most `maxPerMinute` commands are accepted per minute
|
|
20
|
+
* across all conversations; anything past the cap is ignored.
|
|
21
|
+
*
|
|
22
|
+
* Non-command messages (no command prefix) always pass through untouched.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_OPTIONS = Object.freeze({
|
|
26
|
+
enabled: true,
|
|
27
|
+
reactDelayMs: [2000, 3000],
|
|
28
|
+
cooldownMs: 5500,
|
|
29
|
+
resetMs: 10000,
|
|
30
|
+
maxPerMinute: 20,
|
|
31
|
+
commands: ["/", "!", "."],
|
|
32
|
+
perThread: true
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function isCommandLike(body, prefixes) {
|
|
36
|
+
if (typeof body !== "string") return false;
|
|
37
|
+
const text = body.trim();
|
|
38
|
+
if (!text || !Array.isArray(prefixes) || !prefixes.length) return false;
|
|
39
|
+
return prefixes.some(p => typeof p === "string" && p.length > 0 && text.startsWith(p));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function randomBetween(min, max) {
|
|
43
|
+
if (Array.isArray(min)) {
|
|
44
|
+
const pair = min;
|
|
45
|
+
min = pair[0];
|
|
46
|
+
max = pair[1];
|
|
47
|
+
}
|
|
48
|
+
min = Number(min) || 0;
|
|
49
|
+
max = Number(max) || min;
|
|
50
|
+
if (max < min) { const t = min; min = max; max = t; }
|
|
51
|
+
return min + Math.floor(Math.random() * (max - min + 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class CommandHumanizer {
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.setOptions(options);
|
|
57
|
+
this._threads = new Map(); // key -> state
|
|
58
|
+
this._global = { windowStart: 0, count: 0, accepted: 0, dropped: 0 };
|
|
59
|
+
this._eventCount = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setOptions(options) {
|
|
63
|
+
const o = options || {};
|
|
64
|
+
this.enabled = o.enabled !== false;
|
|
65
|
+
this.reactDelayMs = o.reactDelayMs || DEFAULT_OPTIONS.reactDelayMs;
|
|
66
|
+
this.cooldownMs = typeof o.cooldownMs === "number" ? o.cooldownMs : DEFAULT_OPTIONS.cooldownMs;
|
|
67
|
+
this.resetMs = typeof o.resetMs === "number" ? o.resetMs : DEFAULT_OPTIONS.resetMs;
|
|
68
|
+
this.maxPerMinute = typeof o.maxPerMinute === "number" ? o.maxPerMinute : DEFAULT_OPTIONS.maxPerMinute;
|
|
69
|
+
this.prefixes = o.commands || o.prefixes || DEFAULT_OPTIONS.commands;
|
|
70
|
+
this.perThread = o.perThread !== false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isCommand(event) {
|
|
74
|
+
return Boolean(event && isCommandLike(event.body, this.prefixes));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_keyFor(event) {
|
|
78
|
+
return this.perThread && event.threadID ? String(event.threadID) : "_global";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_globalAllowed() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (now - this._global.windowStart >= 60000) {
|
|
84
|
+
this._global.windowStart = now;
|
|
85
|
+
this._global.count = 0;
|
|
86
|
+
}
|
|
87
|
+
if (this._global.count >= this.maxPerMinute) return false;
|
|
88
|
+
this._global.count++;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Decide what to do with a command event, based purely on arrival time
|
|
94
|
+
* (the emission delay is applied separately by the middleware).
|
|
95
|
+
*
|
|
96
|
+
* @returns {{ok: boolean, delayMs: number, reason?: string}}
|
|
97
|
+
*/
|
|
98
|
+
shouldProcess(event) {
|
|
99
|
+
this._eventCount++;
|
|
100
|
+
if (!this.enabled) return { ok: true, delayMs: 0 };
|
|
101
|
+
|
|
102
|
+
const key = this._keyFor(event);
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
|
|
105
|
+
let st = this._threads.get(key);
|
|
106
|
+
if (!st) {
|
|
107
|
+
st = { acceptedAt: 0, coolUntil: 0, lastCmdAt: 0, accepted: 0, dropped: 0 };
|
|
108
|
+
this._threads.set(key, st);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Idle reset: no command in this conversation for resetMs -> the next
|
|
112
|
+
// command starts a fresh window instead of inheriting the old cooldown.
|
|
113
|
+
if (st.lastCmdAt > 0 && (now - st.lastCmdAt >= this.resetMs)) {
|
|
114
|
+
st.acceptedAt = 0;
|
|
115
|
+
st.coolUntil = 0;
|
|
116
|
+
}
|
|
117
|
+
st.lastCmdAt = now;
|
|
118
|
+
|
|
119
|
+
if (st.coolUntil > 0 && now < st.coolUntil) {
|
|
120
|
+
st.dropped++;
|
|
121
|
+
this._global.dropped++;
|
|
122
|
+
return { ok: false, delayMs: 0, reason: "cooldown" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.maxPerMinute > 0 && !this._globalAllowed()) {
|
|
126
|
+
st.dropped++;
|
|
127
|
+
this._global.dropped++;
|
|
128
|
+
return { ok: false, delayMs: 0, reason: "global-cap" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
st.acceptedAt = now;
|
|
132
|
+
st.coolUntil = now + this.cooldownMs;
|
|
133
|
+
st.accepted++;
|
|
134
|
+
this._global.accepted++;
|
|
135
|
+
return { ok: true, delayMs: randomBetween(this.reactDelayMs) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
reset(key) {
|
|
139
|
+
if (key === undefined) { this._threads.clear(); return; }
|
|
140
|
+
this._threads.delete(String(key));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
stats() {
|
|
144
|
+
const threads = [];
|
|
145
|
+
this._threads.forEach((st, k) => threads.push({ threadID: k, ...st }));
|
|
146
|
+
return {
|
|
147
|
+
enabled: this.enabled,
|
|
148
|
+
global: { ...this._global },
|
|
149
|
+
eventCount: this._eventCount,
|
|
150
|
+
threads
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createCommandHumanizer(options) {
|
|
156
|
+
return new CommandHumanizer(options);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Middleware for the FCA middleware system (api.useMiddleware).
|
|
161
|
+
* - Non-commands: forwarded immediately.
|
|
162
|
+
* - Dropped commands: next(false) so the bot never sees them.
|
|
163
|
+
* - Accepted commands: forwarded after the human reaction delay.
|
|
164
|
+
*/
|
|
165
|
+
function createHumanizerMiddleware(humanizer, logger) {
|
|
166
|
+
const log = typeof logger === "function"
|
|
167
|
+
? (msg, level) => logger(msg, level || "info")
|
|
168
|
+
: () => {};
|
|
169
|
+
|
|
170
|
+
return function commandHumanizerMiddleware(event, next) {
|
|
171
|
+
if (!humanizer.isCommand(event)) { next(); return; }
|
|
172
|
+
|
|
173
|
+
const decision = humanizer.shouldProcess(event);
|
|
174
|
+
|
|
175
|
+
if (!decision.ok) {
|
|
176
|
+
const snippet = typeof event.body === "string" ? event.body.slice(0, 40) : String(event.body || "");
|
|
177
|
+
log(`Humanizer: dropped command "${snippet}" (${decision.reason})`, "warn");
|
|
178
|
+
next(false); // stop processing, do not emit
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const delay = Math.max(0, decision.delayMs || 0);
|
|
183
|
+
if (delay <= 0) { next(); return; }
|
|
184
|
+
|
|
185
|
+
// Forward the event after a human-like delay. setTimeout is used
|
|
186
|
+
// (instead of an async function) because this middleware system calls
|
|
187
|
+
// next() itself; returning a promise would make it call next() twice.
|
|
188
|
+
setTimeout(() => next(), delay);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
CommandHumanizer,
|
|
194
|
+
createCommandHumanizer,
|
|
195
|
+
createHumanizerMiddleware,
|
|
196
|
+
randomBetween,
|
|
197
|
+
isCommandLike,
|
|
198
|
+
DEFAULT_OPTIONS
|
|
199
|
+
};
|