glassframe-protocol 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +197 -0
- package/GETTING_STARTED.md +133 -0
- package/LICENSE +56 -0
- package/README.md +145 -0
- package/dist/config.js +138 -0
- package/dist/index.js +13 -0
- package/dist/src/GlassFrame.js +195 -0
- package/dist/src/ai/GroqClient.js +161 -0
- package/dist/src/commands/PrefixRouter.js +177 -0
- package/dist/src/core/Cache.js +98 -0
- package/dist/src/core/EventQueue.js +62 -0
- package/dist/src/core/Layer.js +68 -0
- package/dist/src/core/PerformanceMonitor.js +52 -0
- package/dist/src/core/StateStore.js +69 -0
- package/dist/src/core/ThreatEngine.js +86 -0
- package/dist/src/core/VersionInfo.js +32 -0
- package/dist/src/layers/AIModerationLayer.js +77 -0
- package/dist/src/layers/AntiNukeLayer.js +278 -0
- package/dist/src/layers/AntiRaidLayer.js +144 -0
- package/dist/src/layers/BasicSecurityLayer.js +111 -0
- package/dist/src/logging/ComponentsV2.js +44 -0
- package/dist/src/logging/SmartLogger.js +84 -0
- package/dist/src/moderation/PunishmentEngine.js +175 -0
- package/dist/src/moderation/RoleAnalyzer.js +82 -0
- package/dist/src/security/PhishingDatabase.js +71 -0
- package/dist/src/ui/ControlPanel.js +58 -0
- package/dist/src/utils/nlpEngine.js +149 -0
- package/docs/CACHE_ARCHITECTURE.md +78 -0
- package/docs/COMMANDS.md +38 -0
- package/docs/PERFORMANCE.md +59 -0
- package/docs/PROTOCOL_LAYERS.md +89 -0
- package/docs/PUBLISHING.md +93 -0
- package/examples/basic-usage.js +44 -0
- package/package.json +44 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bounded-concurrency async task queue. Under a real raid or nuke burst,
|
|
5
|
+
* dozens of heavy operations (Groq calls, member fetches, role/ban edits)
|
|
6
|
+
* can all become ready within the same second. Running them all
|
|
7
|
+
* concurrently is exactly what trips Discord's per-route rate limits and
|
|
8
|
+
* makes the bot fall behind right when speed matters most. EventQueue caps
|
|
9
|
+
* how many run at once and queues the rest with no message rejected, no
|
|
10
|
+
* task dropped - so throughput stays smooth under load instead of
|
|
11
|
+
* collapsing into a wall of 429s.
|
|
12
|
+
*
|
|
13
|
+
* Not guild-aware by design: PunishmentEngine and GroqClient each own one
|
|
14
|
+
* shared queue across every guild they serve, since the failure mode this
|
|
15
|
+
* guards against (hammering Discord's/Groq's global rate limits) is global
|
|
16
|
+
* too.
|
|
17
|
+
*/
|
|
18
|
+
class EventQueue {
|
|
19
|
+
constructor({ concurrency = 4 } = {}) {
|
|
20
|
+
this.concurrency = Math.max(1, concurrency);
|
|
21
|
+
this.active = 0;
|
|
22
|
+
this.queue = [];
|
|
23
|
+
this.stats = { queued: 0, completed: 0, failed: 0, maxQueueDepth: 0 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Enqueues an async task (a zero-arg function returning a Promise) and resolves/rejects with its result. */
|
|
27
|
+
push(task) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
this.queue.push({ task, resolve, reject });
|
|
30
|
+
this.stats.queued++;
|
|
31
|
+
this.stats.maxQueueDepth = Math.max(this.stats.maxQueueDepth, this.queue.length);
|
|
32
|
+
this._drain();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_drain() {
|
|
37
|
+
while (this.active < this.concurrency && this.queue.length) {
|
|
38
|
+
const next = this.queue.shift();
|
|
39
|
+
this.active++;
|
|
40
|
+
Promise.resolve()
|
|
41
|
+
.then(next.task)
|
|
42
|
+
.then((result) => {
|
|
43
|
+
this.stats.completed++;
|
|
44
|
+
next.resolve(result);
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => {
|
|
47
|
+
this.stats.failed++;
|
|
48
|
+
next.reject(err);
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
this.active--;
|
|
52
|
+
this._drain();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getStats() {
|
|
58
|
+
return { ...this.stats, active: this.active, pending: this.queue.length, concurrency: this.concurrency };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = EventQueue;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require("events");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base class for every GlassFrame Protocol security layer.
|
|
7
|
+
*
|
|
8
|
+
* A layer owns exactly one security domain (raid, nuke, message security,
|
|
9
|
+
* AI). Layers do not punish members directly - they report signals to the
|
|
10
|
+
* shared ThreatEngine/PunishmentEngine so every layer is judged by the same,
|
|
11
|
+
* smart, non-spammy rulebook (see docs/PROTOCOL_LAYERS.md). Layers CAN take
|
|
12
|
+
* immediate, reversible containment action on the guild itself (raise
|
|
13
|
+
* verification level, revert a role's permissions) since those are
|
|
14
|
+
* time-critical and aren't about penalizing one member.
|
|
15
|
+
*
|
|
16
|
+
* enable()/disable() are the only lifecycle a layer needs to implement -
|
|
17
|
+
* disable() fully detaches every listener the layer attached, so a disabled
|
|
18
|
+
* layer does zero work rather than quietly early-returning inside a handler
|
|
19
|
+
* that's still subscribed.
|
|
20
|
+
*/
|
|
21
|
+
class Layer extends EventEmitter {
|
|
22
|
+
constructor(name, frame) {
|
|
23
|
+
super();
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.frame = frame;
|
|
26
|
+
this.enabled = false;
|
|
27
|
+
this._listeners = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get client() {
|
|
31
|
+
return this.frame.client;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get config() {
|
|
35
|
+
return this.frame.config[this.name] || {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_listen(emitter, event, handler) {
|
|
39
|
+
emitter.on(event, handler);
|
|
40
|
+
this._listeners.push([emitter, event, handler]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
enable() {
|
|
44
|
+
if (this.enabled) return this;
|
|
45
|
+
this.enabled = true;
|
|
46
|
+
this.onEnable();
|
|
47
|
+
this.frame.emit("layerToggled", { layer: this.name, enabled: true });
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
disable() {
|
|
52
|
+
if (!this.enabled) return this;
|
|
53
|
+
this.enabled = false;
|
|
54
|
+
for (const [emitter, event, handler] of this._listeners) {
|
|
55
|
+
emitter.removeListener(event, handler);
|
|
56
|
+
}
|
|
57
|
+
this._listeners = [];
|
|
58
|
+
this.onDisable();
|
|
59
|
+
this.frame.emit("layerToggled", { layer: this.name, enabled: false });
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Subclasses override these two hooks.
|
|
64
|
+
onEnable() {}
|
|
65
|
+
onDisable() {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = Layer;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight internal metrics collector - no external dependency, no
|
|
5
|
+
* network call, just counters and a small rolling latency sample per event
|
|
6
|
+
* type. Exposed via `frame.getMetrics()` / `!gf metrics` so you can see
|
|
7
|
+
* whether the protocol is keeping up on a busy server without wiring in a
|
|
8
|
+
* separate APM tool.
|
|
9
|
+
*/
|
|
10
|
+
class PerformanceMonitor {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.startedAt = Date.now();
|
|
13
|
+
this.eventCounts = {};
|
|
14
|
+
this.latencies = {}; // name -> last 50 durations in ms
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
recordEvent(name) {
|
|
18
|
+
this.eventCounts[name] = (this.eventCounts[name] || 0) + 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Call at the start of a unit of work; call the returned function when it finishes. */
|
|
22
|
+
time(name) {
|
|
23
|
+
const start = process.hrtime.bigint();
|
|
24
|
+
return () => {
|
|
25
|
+
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
|
26
|
+
const arr = this.latencies[name] || (this.latencies[name] = []);
|
|
27
|
+
arr.push(ms);
|
|
28
|
+
if (arr.length > 50) arr.shift();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_avg(name) {
|
|
33
|
+
const arr = this.latencies[name];
|
|
34
|
+
if (!arr || !arr.length) return null;
|
|
35
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
snapshot() {
|
|
39
|
+
const avgLatencyMs = {};
|
|
40
|
+
for (const name of Object.keys(this.latencies)) {
|
|
41
|
+
const avg = this._avg(name);
|
|
42
|
+
avgLatencyMs[name] = avg === null ? null : Math.round(avg * 100) / 100;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
uptimeMs: Date.now() - this.startedAt,
|
|
46
|
+
events: { ...this.eventCounts },
|
|
47
|
+
avgLatencyMs
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = PerformanceMonitor;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default store: nothing persists across restarts. Fine for most setups,
|
|
8
|
+
* since layer state is usually re-armed via autoStart or the control panel.
|
|
9
|
+
*/
|
|
10
|
+
class MemoryStateStore {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.data = new Map();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async get(guildId) {
|
|
16
|
+
return this.data.get(guildId) || null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async set(guildId, state) {
|
|
20
|
+
this.data.set(guildId, state);
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional convenience store backed by a single JSON file - no database,
|
|
27
|
+
* no native module, so it works the same on ARM64/Termux as anywhere else.
|
|
28
|
+
* Fine for the scale this library operates at (per-guild layer toggles),
|
|
29
|
+
* not intended as a general-purpose datastore.
|
|
30
|
+
*/
|
|
31
|
+
class JSONFileStateStore {
|
|
32
|
+
constructor(filePath) {
|
|
33
|
+
this.filePath = filePath || path.join(process.cwd(), "glassframe-state.json");
|
|
34
|
+
this._cache = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_load() {
|
|
38
|
+
if (this._cache) return this._cache;
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(this.filePath, "utf8");
|
|
41
|
+
this._cache = JSON.parse(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
this._cache = {};
|
|
44
|
+
}
|
|
45
|
+
return this._cache;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_persist() {
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this._cache, null, 2));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error("[GlassFrame:StateStore] failed to persist:", err.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async get(guildId) {
|
|
57
|
+
const data = this._load();
|
|
58
|
+
return data[guildId] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async set(guildId, state) {
|
|
62
|
+
const data = this._load();
|
|
63
|
+
data[guildId] = state;
|
|
64
|
+
this._persist();
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { MemoryStateStore, JSONFileStateStore };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require("events");
|
|
4
|
+
const ProtocolCache = require("./Cache");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Central, cross-layer risk aggregator. Every layer reports weighted
|
|
8
|
+
* signals here instead of deciding punishment on its own; ThreatEngine
|
|
9
|
+
* keeps one decaying score per member so a scattering of small, unrelated
|
|
10
|
+
* flags from different layers combines into one picture instead of each
|
|
11
|
+
* separately triggering a reaction, and a single serious flag isn't diluted
|
|
12
|
+
* by history that's long since gone stale.
|
|
13
|
+
*/
|
|
14
|
+
class ThreatEngine extends EventEmitter {
|
|
15
|
+
constructor(config, { debug = false } = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config.threatEngine;
|
|
18
|
+
// No hard TTL - decay handles staleness. _prune() below reclaims memory
|
|
19
|
+
// once an entry has decayed to noise instead of relying on a timed expiry.
|
|
20
|
+
this.scores = new ProtocolCache("threat-scores", { ttlMs: 0, debug, sweepEveryMs: 0 });
|
|
21
|
+
|
|
22
|
+
this._pruner = setInterval(() => this._prune(), 10 * 60 * 1000);
|
|
23
|
+
this._pruner.unref?.();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
key(guildId, userId) {
|
|
27
|
+
return `${guildId}:${userId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_decayed(entry) {
|
|
31
|
+
if (!entry) return 0;
|
|
32
|
+
const elapsed = Date.now() - entry.updatedAt;
|
|
33
|
+
const factor = Math.pow(0.5, elapsed / this.config.decayHalfLifeMs);
|
|
34
|
+
return entry.score * factor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addSignal(guildId, userId, { layer, weight, reason }) {
|
|
38
|
+
const key = this.key(guildId, userId);
|
|
39
|
+
const existing = this.scores.get(key);
|
|
40
|
+
const decayedScore = this._decayed(existing);
|
|
41
|
+
const newScore = Math.min(100, decayedScore + weight);
|
|
42
|
+
const history = existing ? existing.history.slice(-9) : [];
|
|
43
|
+
history.push({ layer, weight, reason, at: Date.now() });
|
|
44
|
+
|
|
45
|
+
this.scores.set(key, { score: newScore, updatedAt: Date.now(), history });
|
|
46
|
+
|
|
47
|
+
const tier = this.getTier(newScore);
|
|
48
|
+
this.emit("signal", { guildId, userId, layer, reason, weight, score: newScore, tier });
|
|
49
|
+
return { score: newScore, tier, history };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getScore(guildId, userId) {
|
|
53
|
+
return this._decayed(this.scores.get(this.key(guildId, userId)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getTier(score) {
|
|
57
|
+
const t = this.config.tiers;
|
|
58
|
+
if (score >= t.critical) return "critical";
|
|
59
|
+
if (score >= t.high) return "high";
|
|
60
|
+
if (score >= t.medium) return "medium";
|
|
61
|
+
if (score >= t.low) return "low";
|
|
62
|
+
return "none";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getHistory(guildId, userId) {
|
|
66
|
+
const entry = this.scores.get(this.key(guildId, userId));
|
|
67
|
+
return entry ? entry.history : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
reset(guildId, userId) {
|
|
71
|
+
this.scores.delete(this.key(guildId, userId));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Drops entries that have decayed to near-zero and haven't moved in a long time. */
|
|
75
|
+
_prune() {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
for (const [key, entry] of this.scores.store) {
|
|
78
|
+
const decayed = this._decayed(entry);
|
|
79
|
+
if (decayed < 0.5 && now - entry.updatedAt > this.config.decayHalfLifeMs * 4) {
|
|
80
|
+
this.scores.delete(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = ThreatEngine;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const VERSION = "1.2.0";
|
|
4
|
+
const NAME = "GlassFrame Protocol";
|
|
5
|
+
const RELEASED = "2026-07-31";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A plain-ASCII banner (no emoji, no box-drawing Unicode) so it renders
|
|
9
|
+
* identically in every terminal, including Termux. Printed once at startup
|
|
10
|
+
* when GlassFrame is constructed with { debug: true }.
|
|
11
|
+
*/
|
|
12
|
+
function banner() {
|
|
13
|
+
const title = `${NAME} v${VERSION}`;
|
|
14
|
+
const subtitle = "Layered Discord Security Engine";
|
|
15
|
+
const width = Math.max(title.length, subtitle.length) + 4;
|
|
16
|
+
|
|
17
|
+
const bar = "+" + "-".repeat(width - 2) + "+";
|
|
18
|
+
const center = (text) => {
|
|
19
|
+
const spare = width - 2 - text.length;
|
|
20
|
+
const left = Math.floor(spare / 2);
|
|
21
|
+
const right = spare - left;
|
|
22
|
+
return "|" + " ".repeat(left) + text + " ".repeat(right) + "|";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return [bar, center(title), center(subtitle), bar].join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function info() {
|
|
29
|
+
return { name: NAME, version: VERSION, released: RELEASED };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { VERSION, NAME, RELEASED, banner, info };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const Layer = require("../core/Layer");
|
|
4
|
+
const { classifyMessage } = require("../utils/nlpEngine");
|
|
5
|
+
|
|
6
|
+
const SEVERITY_WEIGHT = {
|
|
7
|
+
spam: 15,
|
|
8
|
+
harassment: 30,
|
|
9
|
+
scam: 40,
|
|
10
|
+
phishing: 40,
|
|
11
|
+
raid_coordination: 45,
|
|
12
|
+
nsfw: 35
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Two-stage message moderation: the free, local NLP classifier runs first
|
|
17
|
+
* and handles the obvious cases. Only messages that land in the gray zone -
|
|
18
|
+
* not clean, not obviously over threshold - get escalated to Groq for a
|
|
19
|
+
* second, smarter opinion. This keeps AI calls rare, cheap, and reserved
|
|
20
|
+
* for the messages that actually need judgement, and keeps this layer fully
|
|
21
|
+
* independent of BasicSecurityLayer so either can run without the other.
|
|
22
|
+
*/
|
|
23
|
+
class AIModerationLayer extends Layer {
|
|
24
|
+
constructor(frame) {
|
|
25
|
+
super("aiModeration", frame);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get groq() {
|
|
29
|
+
return this.frame.groqClient;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onEnable() {
|
|
33
|
+
if (!this.groq?.available) {
|
|
34
|
+
this.frame.emit("warning", {
|
|
35
|
+
layer: "aiModeration",
|
|
36
|
+
message: "No Groq API keys configured - layer is active but will stay idle. Set aiModeration.apiKeys in config."
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
this._listen(this.client, "messageCreate", (message) => this._handle(message).catch(() => {}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onDisable() {}
|
|
43
|
+
|
|
44
|
+
async _handle(message) {
|
|
45
|
+
if (!message.guild || message.author.bot || !message.content) return;
|
|
46
|
+
if (!this.groq?.available) return;
|
|
47
|
+
|
|
48
|
+
const cfg = this.config;
|
|
49
|
+
const local = classifyMessage(message.content);
|
|
50
|
+
const topScore = Math.max(local.scam, local.phishing, local.raidCallout);
|
|
51
|
+
|
|
52
|
+
const inGrayZone = topScore >= cfg.grayZone.min && topScore < cfg.grayZone.max;
|
|
53
|
+
if (!inGrayZone) return;
|
|
54
|
+
|
|
55
|
+
const verdict = await this.groq.classify(message.content, `local pre-scan suggested: ${local.topLabel}`);
|
|
56
|
+
if (!verdict || verdict.category === "clean") return;
|
|
57
|
+
|
|
58
|
+
const weight = Math.round((SEVERITY_WEIGHT[verdict.category] || 20) * verdict.confidence);
|
|
59
|
+
if (weight <= 0) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (message.deletable) await message.delete();
|
|
63
|
+
} catch {
|
|
64
|
+
/* best effort */
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await this.frame.punishmentEngine.report({
|
|
68
|
+
guild: message.guild,
|
|
69
|
+
member: message.member,
|
|
70
|
+
layer: "aiModeration",
|
|
71
|
+
weight,
|
|
72
|
+
reason: `AI flagged ${verdict.category} (${Math.round(verdict.confidence * 100)}%): ${verdict.reason}`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = AIModerationLayer;
|