Moderation Engine
A rule engine that acts on VK activity without the model in the loop. It exists because a community needs consistent, instant moderation that does not depend on a language model being awake, in budget, or in the mood.
Rule shape
interface Rule {
id: string // stable, human-readable: 'spam-links', 'welcome-ban'
enabled: boolean
match: {
type: 'keyword' | 'regex' | 'newMember' | 'link' | 'externalLink' | 'repeat'
pattern?: string // keyword list (comma-separated) or regex source
flags?: string // regex flags, default 'iu'
scope: Array<'message' | 'comment' | 'wallPost' | 'boardReply'>
}
action: {
kind: 'ban' | 'delete' | 'warn' | 'notify'
durationH?: number // 0 or absent = permanent
reason?: number // VK ban reason code
comment?: string // shown in the ban list
notifyText?: string // for 'notify'
}
exempt?: {
userIds?: number[]
minAdminLevel?: number // never act on users at or above this level
ignoreStaff?: boolean // default true
}
}
Match types
| Type | Behaviour |
|---|---|
keyword |
case-insensitive substring over a comma-separated list; word-boundary aware for Cyrillic and Latin |
regex |
user-supplied source + flags; compiled once at load, invalid regex fails the rule with a clear error (not a silent skip) |
newMember |
fires only when the author joined within pattern minutes (default 10) — the classic spam-bot signature |
link |
any URL in the text |
externalLink |
a URL whose host is not in the community's links block |
repeat |
the same user posted ≥ N times (N from pattern) within 10 minutes |
Evaluation order
Rules evaluate in declaration order. First match wins for actions, but warn rules accumulate: a user can collect warnings from multiple rules before hitting the threshold.
event arrives
→ exempt check (userIds, admin level, staff)
→ for each enabled rule whose scope includes the event type:
match? → execute action
→ write audit
→ if action is 'warn': increment counter, check threshold
→ deliver event to agent (always, with an auto-moderated prefix if anything fired)
Warn threshold
moderation.warnThreshold (default 3). Counters live in ctx.storage under vk.warn.<userId> with a rolling 30-day window. On reaching the threshold:
- The user is banned for
durationHfrom the threshold-crossing rule, or permanently if unset. - The counter resets.
- An audit record notes
trigger: 'warn-threshold'.
Where it runs
Two entry points, same engine:
- Inbound — on every LongPoll event before delivery to the agent (
events/inbound.ts). - Outbound — as a
tools/pre-executelistener, checking posts/comments the agent itself is about to create. A rule that would ban a user also blocks the agent from posting content that matches adeleterule — the agent cannot launder banned content through its own tools.
The engine never calls the model. It calls VK API directly through the same client, so rate limiting and error handling are shared.
Storage
| Key | Content |
|---|---|
vk.rules |
Rule[] — the full rule set |
vk.warn.<userId> |
{ count: number; firstAt: number; lastAt: number } |
vk.audit |
append-only array of audit records (bounded to last 10 000, rotated) |
Rules are edited through vk_moderation_rules, not by hand-editing storage. Every write is validated against the Rule schema before it lands.
Audit record
interface AuditRecord {
at: number // unix ms
source: 'tool' | 'rule' | 'longpoll'
tool?: string // e.g. 'vk_ban_manage'
ruleId?: string
action: string
target?: { userIds?: number[]; postId?: number; commentId?: number; topicId?: number }
before?: unknown // for settings changes
after?: unknown
ok: boolean
error?: string
token: '<redacted>' // never the real token
}
The audit log is written from tools/result (a final observer) for tool calls and directly by the engine for rule actions. It is append-only from the plugin's perspective — no tool deletes audit entries.
Safety invariants
- Never act on staff.
exempt.ignoreStaffdefaults to true; the engine resolvesadmin_levelfor the target before acting and skips anyone at or aboveminAdminLevel(default 1). - Never act on the community's own id or on ids in
moderation.exemptUserIds. - Never ban without a reason code — VK requires one for visible bans, and an unexplained ban is a support ticket.
deleteis preferred overbanfor first offences unless the rule says otherwise; deletion is reversible, bans are not (from the user's view).- Dry-run mode.
moderation.enabled: trueplussafety.dryRunDefault: truemeans rules evaluate and audit but do not execute. Use this to tune a rule set against a week of real traffic before turning it on.
Example rule set
[
{
"id": "spam-links",
"enabled": true,
"match": { "type": "externalLink", "scope": ["message", "comment", "wallPost"] },
"action": { "kind": "delete", "comment": "Внешние ссылки запрещены" },
"exempt": { "ignoreStaff": true }
},
{
"id": "new-member-crypto",
"enabled": true,
"match": { "type": "keyword", "pattern": "крипт,казино,ставки,заработок", "scope": ["message", "comment"] },
"action": { "kind": "ban", "durationH": 720, "reason": 1, "comment": "Спам" },
"exempt": { "ignoreStaff": true }
},
{
"id": "first-post-welcome",
"enabled": true,
"match": { "type": "newMember", "pattern": "10", "scope": ["wallPost"] },
"action": { "kind": "notify", "notifyText": "Проверьте первый пост новичка" }
}
]
Limitations
- No ML classification. Keyword and regex only — deliberate: predictable, auditable, cheap.
- No cross-community reputation. Each community's engine is independent.
- No automatic appeal flow. A banned user must contact the community through other means.
- Rule ordering matters and is not visible in the UI beyond the list order. Keep rule ids descriptive.