feat: rate limiting + suspicious link filtering (#39)#93
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (23)
📝 WalkthroughWalkthroughThis PR implements two new moderation detection layers integrated into the MessageCreate handler: rate limiting tracks messages per user per channel using a sliding window, while link filtering detects suspicious URLs through phishing pattern matching and domain blocklists. Both modules support admin/mod exemptions and moderation alerts. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Created PR with unit tests: #96 |
|
| Filename | Overview |
|---|---|
| src/modules/rateLimit.js | New rate limiting module with sliding window tracking, escalation to temp-mute, and memory-efficient eviction strategy - well-tested and properly integrated |
| src/modules/linkFilter.js | New link filtering module with phishing detection and configurable blocklist - solid regex patterns and proper URL extraction logic |
| src/utils/modExempt.js | New shared utility for mod/admin exemption checking - consolidates permission logic used by rate limiter and link filter |
| src/modules/events.js | Integrated rate limit and link filter checks into message pipeline before spam detection and AI triage - proper error handling with isolated try-catch blocks |
| tests/modules/rateLimit.test.js | Comprehensive test coverage (342 lines) for sliding window, exemptions, repeat offender muting, and memory cap - uses fake timers appropriately |
| tests/modules/linkFilter.test.js | Comprehensive test coverage (353 lines) for URL extraction, phishing pattern detection, blocklist matching, and exemptions |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Message Received] --> B{moderation.enabled?}
B -->|No| G[Continue to AI Triage]
B -->|Yes| C[Check Rate Limit]
C --> D{Rate Limited?}
D -->|Yes| E[Delete Message]
E --> F{Repeat Offender?}
F -->|Yes| F1[Apply Timeout + Alert Mods]
F -->|No, First Trigger| F2[Warn User]
F1 --> END[Stop Processing]
F2 --> END
D -->|No| H[Check Link Filter]
H --> I{Blocked Link?}
I -->|Yes| J[Delete + Alert Mods]
J --> END
I -->|No| K[Check Spam Patterns]
K --> L{Is Spam?}
L -->|Yes| M[Alert Mods]
M --> END
L -->|No| G
G --> N[AI Triage Evaluation]
Last reviewed commit: e52101b
|
Claude finished @BillChirico's task in 3m 13s —— View job Code Review — PR #93: Rate Limiting + Link Filtering
SummaryThe implementation is solid — sliding window rate limiting with progressive escalation, phishing pattern detection, domain blocklist filtering, shared exemption utility, error isolation in the event pipeline, and comprehensive test coverage (including eviction and mixed-case normalization). Most issues from previous reviews have been addressed. 3 remaining issues — all documentation gaps: 🟡 Warnings
🔵 Nitpick
🤖 AI fix prompt — copy and paste to fix all issues |
There was a problem hiding this comment.
Issues Found
🟡 Warnings (3)
-
rateLimit.js:110-113— Rawmessage.reply()bypassessafeReply(): Per project conventions, all outgoing messages must use safe wrappers to enforceallowedMentionsand sanitize mentions. The warning reply uses rawmessage.reply(). -
rateLimit.js:50/linkFilter.js:81—config.permissions.modRolesdoesn't exist inconfig.json: Both modules rely onconfig.permissions?.modRolesfor mod exemptions, butconfig.jsononly haspermissions.adminRoleIdandpermissions.moderatorRoleId(singular strings). The mod-role exemption path is effectively dead code. Either addmodRoles: []to config.json or use the existing fields. -
Missing
rateLimitandlinkFilterdefaults inconfig.json: Per AGENTS.md, new modules must have their config section added toconfig.json. Users can't discover these options without it. Also, AGENTS.md Key Files table and README.md should be updated per documentation requirements.
🔵 Nitpicks (3)
-
Duplicated
isExemptfunction — Identical logic in bothrateLimit.js:43-54andlinkFilter.js:75-87. Should be extracted to a shared utility. -
Unsanitized user content in embeds — Both modules embed
message.author.tagand message content in alert embeds without sanitization. Low risk (embed fields don't trigger pings) but inconsistent with the project's defense-in-depth pattern. -
Redundant
moderation.enabledcheck inevents.js— Two separate identicalifblocks can be combined.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/modules/events.js`:
- Around line 98-108: Wrap the two moderation pre-checks (the calls to
checkRateLimit(message, guildConfig) and checkLinks(message, guildConfig)
guarded by guildConfig.moderation?.enabled) in a local try/catch so any thrown
error is caught and the listener doesn't reject; in the catch, call your logger
(or processLogger) with context including the function/listener name, the
message id or author, and guild id, then safely continue (or return) without
rethrowing so a single failing check cannot crash the event handler.
In `@src/modules/linkFilter.js`:
- Around line 148-156: Normalize configured blocked domain entries before
matching: when building blockedDomains (used in the link filtering loop that
calls extractUrls and compares hostname to blocked), map lfConfig.blockedDomains
to a normalized list by trimming whitespace, lowercasing, removing a leading
"www." (and any trailing dot), and then use that normalized array in the
matching logic; keep hostname normalization as-is and compare hostname ===
blocked || hostname.endsWith(`.${blocked}`) against the normalizedBlocked list
instead of the raw lfConfig entries.
In `@src/modules/rateLimit.js`:
- Around line 62-63: The mute alert embed currently hard-codes "3 triggers in 5
minutes"; update handleRepeatOffender (and the other embed constructions at the
locations referenced) to build the reason string from the config values instead:
use config.triggerCount and convert the window (e.g., config.windowMinutes or
config.windowMs -> minutes) dynamically and include the muteDurationMs
(formatted to minutes/seconds as appropriate) so the embed reason reflects the
actual config (e.g., `${config.triggerCount} triggers in ${windowMinutes}
minutes`), replacing the hard-coded text wherever it appears (lines around
handleRepeatOffender and the other two embed usages).
- Around line 109-113: In warnUser, don't call message.reply directly; replace
that raw reply with the project's safe message helper (e.g., safeSend or
safeReply) to ensure mention sanitization and allowedMentions enforcement:
update the import/require to pull in the safe helper, call safeSend/safeReply in
place of message.reply with the same composed text, and preserve the existing
catch behavior (returning null) so behavior and error handling remain
consistent; reference the warnUser function and the safeSend/safeReply helper
when making the change.
In `@tests/modules/linkFilter.test.js`:
- Around line 185-249: Add a regression test in the existing "checkLinks —
blocklist matching" suite to verify mixed-case entries in blockedDomains are
matched case-insensitively: create a config via makeConfig({ blockedDomains:
['Evil.COM'] }), a message via makeMessage({ content: 'https://evil.com/path'
}), call checkLinks(msg, config) and assert result.blocked is true (and
optionally expect(msg.delete).toHaveBeenCalledTimes(1)). Place the new it(...)
near the other blocklist tests so it uses the same helpers (checkLinks,
makeConfig, makeMessage) and ensures domain matching is normalized.
In `@tests/modules/rateLimit.test.js`:
- Around line 292-315: The test never triggers eviction because it never exceeds
the module's MAX_TRACKED_USERS; update the test to actually exercise eviction by
either (a) temporarily lowering the module's MAX_TRACKED_USERS (mock or set the
exported constant) before the loop so checkRateLimit is called enough times to
exceed that smaller cap and then assert getTrackedCount() <= that cap, or (b)
modify the rate-limit implementation to accept a cap via config (e.g., add a
maxTrackedUsers option consumed by checkRateLimit) and in the test pass a small
cap so the eviction path runs; target the checkRateLimit, getTrackedCount and
MAX_TRACKED_USERS (or new config option) symbols when making the change.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
src/modules/events.jssrc/modules/linkFilter.jssrc/modules/rateLimit.jstests/modules/linkFilter.test.jstests/modules/rateLimit.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
**/*.js: Use ESM modules only — useimport/export, neverrequire()
Usenode:protocol for Node.js builtins (e.g.import { readFileSync } from 'node:fs')
Always use semicolons
Use single quotes for strings
Use 2-space indentation
No TypeScript — use plain JavaScript with JSDoc comments for documentation
Files:
src/modules/events.jssrc/modules/rateLimit.jstests/modules/linkFilter.test.jssrc/modules/linkFilter.jstests/modules/rateLimit.test.js
src/**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.js: Always use Winston for logging — import{ info, warn, error }from../logger.js
NEVER useconsole.log,console.warn,console.error, or anyconsole.*method in src/ files
Pass structured metadata to Winston logging calls (e.g.info('Message processed', { userId, channelId }))
Use custom error classes fromsrc/utils/errors.jsfor error handling
Always log errors with context before re-throwing
UsegetConfig(guildId?)fromsrc/modules/config.jsto read config
UsesetConfigValue(path, value, guildId?)fromsrc/modules/config.jsto update config at runtime
UsesplitMessage()utility for messages exceeding Discord's 2000-character limit
UsesafeSend()wrapper for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Files:
src/modules/events.jssrc/modules/rateLimit.jssrc/modules/linkFilter.js
src/modules/events.js
📄 CodeRabbit inference engine (AGENTS.md)
Register event handlers in
src/modules/events.jsby callingclient.on()with the event name and handler function
Files:
src/modules/events.js
tests/**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
Test files must achieve at least 80% code coverage on statements, branches, functions, and lines
Files:
tests/modules/linkFilter.test.jstests/modules/rateLimit.test.js
🧠 Learnings (7)
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/**/*.js : Use `safeSend()` wrapper for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Applied to files:
src/modules/events.jssrc/modules/rateLimit.jssrc/modules/linkFilter.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must call `checkHierarchy(moderator, target)` before executing mod actions to prevent moderating users with equal or higher roles
Applied to files:
src/modules/events.jssrc/modules/rateLimit.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Send DM notifications to moderation targets *before* executing kicks/bans (once kicked/banned, users cannot receive DMs from the bot)
Applied to files:
src/modules/events.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*mod*.js : Moderation commands must follow the pattern: deferReply → validate inputs → sendDmNotification → execute Discord action → createCase → sendModLogEmbed → checkEscalation
Applied to files:
src/modules/events.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility for messages exceeding Discord's 2000-character limit
Applied to files:
src/modules/rateLimit.jssrc/modules/linkFilter.jstests/modules/rateLimit.test.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/modules/triage.js : Triage tracks at most 100 channels; channels inactive for 30 minutes are evicted from the buffer
Applied to files:
src/modules/rateLimit.js
📚 Learning: 2026-02-25T02:39:33.506Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-25T02:39:33.506Z
Learning: Applies to src/commands/*timeout*.js : Discord timeouts max at 28 days; enforce this cap in timeout commands
Applied to files:
src/modules/rateLimit.jstests/modules/rateLimit.test.js
🧬 Code graph analysis (5)
src/modules/events.js (2)
src/modules/rateLimit.js (1)
checkRateLimit(129-204)src/modules/linkFilter.js (1)
checkLinks(125-172)
src/modules/rateLimit.js (2)
src/logger.js (2)
warn(237-239)info(230-232)src/utils/safeSend.js (1)
safeSend(116-123)
tests/modules/linkFilter.test.js (2)
src/modules/linkFilter.js (5)
member(76-76)results(37-37)extractUrls(36-54)matchPhishingPattern(61-67)checkLinks(125-172)src/utils/permissions.js (1)
isAdmin(52-71)
src/modules/linkFilter.js (3)
tests/modules/linkFilter.test.js (1)
member(29-38)src/utils/safeSend.js (1)
safeSend(116-123)src/logger.js (1)
warn(237-239)
tests/modules/rateLimit.test.js (2)
src/modules/rateLimit.js (5)
member(44-44)member(63-63)i(28-28)checkRateLimit(129-204)getTrackedCount(217-219)src/utils/permissions.js (1)
isAdmin(52-71)
🪛 GitHub Actions: CI
src/modules/linkFilter.js
[error] 16-16: lint/complexity/noUselessEscapeInRegex: Unescape the character in URL_REGEX.
🔇 Additional comments (1)
src/modules/linkFilter.js (1)
16-16: Regex escape has already been fixed in the current code.The
URL_REGEXat line 16 already uses the correct form[a-z0-9-](without useless escaping), so the proposed fix is already applied. ThenoUselessEscapeInRegexlint rule should not be violated by this code.Likely an incorrect or invalid review comment.
Additional Comments (3)
"rateLimit": {
"enabled": false,
"maxMessages": 10,
"windowSeconds": 10,
"muteAfterTriggers": 3,
"muteWindowSeconds": 300,
"muteDurationSeconds": 300
},
"linkFilter": {
"enabled": false,
"blockedDomains": []
}Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: config.json
Line: 53-80
Comment:
missing `rateLimit` and `linkFilter` configuration sections - add these to the moderation config so users know how to configure them:
```json
"rateLimit": {
"enabled": false,
"maxMessages": 10,
"windowSeconds": 10,
"muteAfterTriggers": 3,
"muteWindowSeconds": 300,
"muteDurationSeconds": 300
},
"linkFilter": {
"enabled": false,
"blockedDomains": []
}
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: README.md
Line: 181-201
Comment:
document new `rateLimit` and `linkFilter` config options in the moderation section - users need to know how to configure these new features
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! Prompt To Fix With AIThis is a comment left during a code review.
Path: AGENTS.md
Line: 20-37
Comment:
add the new modules to the Key Files table:
- `src/modules/rateLimit.js` — Rate limiting — sliding window per user/channel with progressive escalation (delete → warn → temp-mute)
- `src/modules/linkFilter.js` — Link filtering — domain blocklist + phishing pattern detection
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise. |
There was a problem hiding this comment.
Review Summary — 6 Warnings, 3 Nitpicks
🟡 Warnings
-
src/modules/rateLimit.js:111-117— Rawmessage.reply()bypassessafeReply()
Per AGENTS.md, all outgoing messages must use safe wrappers. Import and usesafeReplyinstead. -
src/modules/rateLimit.js:50+src/modules/linkFilter.js:81—config.permissions.modRolesdoesn't exist inconfig.json
Both modules checkconfig.permissions?.modRoles(an array), but config only hasadminRoleId/moderatorRoleId(singular strings). The mod-role exemption path is dead code. Consider reusingisModerator()fromsrc/utils/permissions.js. -
src/modules/events.js:98-108— No error isolation around moderation pre-checks
IfcheckRateLimitorcheckLinksthrows, the entire MessageCreate handler rejects. Wrap in try/catch. -
src/modules/linkFilter.js:150— Blocklist domain comparison is case-sensitive
hostnameis lowercased butblockedDomainsconfig entries are not normalized.Evil.COMwon't matchevil.com. -
src/modules/rateLimit.js:98— Hard-coded "3 triggers in 5 minutes" in mute embed
These values come from config. Build the reason string dynamically. -
config.json+AGENTS.md— Missing config defaults and documentation
Per AGENTS.md: new modules need config defaults inconfig.json, entries in the Key Files table, and README.md documentation. AddrateLimitandlinkFiltersections tomoderationinconfig.json(disabled by default), update AGENTS.md Key Files table, and document the new config options in README.md.
🔵 Nitpicks
-
src/modules/rateLimit.js:69— Inconsistent permission flag usage
Uses string'ModerateMembers'instead ofPermissionFlagsBits.ModerateMembers. -
src/modules/rateLimit.js:43-56+src/modules/linkFilter.js:75-87— DuplicatedisExemptfunction
Identical logic in both files. Extract to a shared utility. -
tests/modules/rateLimit.test.js:296-319— Memory cap test doesn't exercise eviction
Creates only 50 entries but eviction triggers at 10,000. The eviction code path has zero test coverage.
🤖 AI fix prompt — copy and paste to fix all issues
Fix the following issues on branch feat/rate-limit-link-filter in VolvoxLLC/volvox-bot:
1. src/modules/rateLimit.js:111-117 — Replace `message.reply()` with `safeReply(message, ...)` from `../utils/safeSend.js`. Add `safeReply` to the import on line 9.
2. src/modules/rateLimit.js:50 and src/modules/linkFilter.js:81 — The `isExempt` function checks `config.permissions?.modRoles` which doesn't exist in config.json. Either: (a) add `"modRoles": []` to the `permissions` section of `config.json`, or (b) refactor `isExempt` to use the existing `isModerator()` from `src/utils/permissions.js` which already checks `adminRoleId`, `moderatorRoleId`, and `PermissionFlagsBits.Administrator`.
3. src/modules/events.js:98-108 — Wrap both moderation pre-checks (checkRateLimit + checkLinks) in a single `if (guildConfig.moderation?.enabled)` block with a try/catch. In the catch, call `logError('Moderation pre-check failed', { guildId, channelId, userId, error })`.
4. src/modules/linkFilter.js:150 — Normalize blockedDomains entries: `.map(d => d.toLowerCase().trim()).filter(Boolean)`.
5. src/modules/rateLimit.js:98 — Replace hard-coded "3 triggers in 5 minutes" with values from config: `${muteThreshold} triggers in ${Math.round(muteWindowSeconds / 60)} minute(s)`. Thread muteThreshold and muteWindowSeconds into handleRepeatOffender.
6. src/modules/rateLimit.js:69 — Change `'ModerateMembers'` to `PermissionFlagsBits.ModerateMembers`.
7. Extract the duplicated `isExempt` function from rateLimit.js:43-56 and linkFilter.js:75-87 into a shared utility in src/utils/ and import it in both files.
8. config.json — Add rateLimit and linkFilter config sections inside the "moderation" block (after line 80), both with "enabled": false by default.
9. AGENTS.md — Add these entries to the Key Files table:
- `src/modules/rateLimit.js` | Rate limiting — sliding window per user/channel with progressive escalation
- `src/modules/linkFilter.js` | Link filtering — domain blocklist + phishing pattern detection
10. tests/modules/rateLimit.test.js:296-319 — Make the memory cap test actually trigger eviction. Either export a setter for MAX_TRACKED_USERS from rateLimit.js for testing, or create 10,001 unique users in the test to trigger the eviction path, then assert getTrackedCount() <= 10000.
11. tests/modules/linkFilter.test.js — Add a regression test for mixed-case blockedDomains: `makeConfig({ blockedDomains: ['Evil.COM'] })` with message `'https://evil.com/path'` should still block.
- Sliding window per user/channel (default: 10 msgs/10s)
- Config: moderation.rateLimit.{enabled,maxMessages,windowSeconds}
- Actions: delete excess msg, warn user (auto-delete after 10s)
- Repeat offender: temp-mute after 3 triggers in 5 min
- Exempt ADMINISTRATOR permission + modRoles
- Memory cap: evicts 10% when 10,000 tracked users reached
- Exports: checkRateLimit(), clearRateLimitState(), getTrackedCount()
- 13 tests: sliding window, exemptions, mute escalation, memory cap
- Extracts URLs via regex, checks against moderation.linkFilter.blockedDomains - Subdomain matching (blocklist 'evil.com' catches 'sub.evil.com') - Phishing patterns: .xyz domains with discord/nitro/free keywords, discord-nitro/free-nitro subdomains, steamgift domains - Actions: delete message, alert mod channel with embed - Exempt ADMINISTRATOR permission + modRoles - Exports: checkLinks(), extractUrls(), matchPhishingPattern() - 34 tests: URL extraction, blocklist, phishing, exemptions, edge cases
…39) Order: rate limit → link filter → spam patterns → AI triage Both checks are gated on moderation.enabled and return early on match.
Adds permission guard before calling member.timeout() to prevent crashes when the bot lacks MODERATE_MEMBERS permission. Updates test mock to include guild.members.me reference.
- Use safeReply instead of raw message.reply in warnUser (rateLimit.js) - Normalize blockedDomains entries (lowercase, strip www.) in linkFilter.js - Wrap checkRateLimit and checkLinks in isolated try/catch in events.js - Fix memory-cap test to actually exercise eviction via setMaxTrackedUsers - Replace hard-coded '3 triggers in 5 minutes' with actual config values - Sanitize message.author.tag and content in embed fields via sanitizeMentions - Extract shared isExempt to src/utils/modExempt.js; support both modRoles[] and adminRoleId/moderatorRoleId config schemas - Combine duplicate moderation?.enabled blocks in events.js - Use PermissionFlagsBits.ModerateMembers instead of string literal - Add mixed-case blockedDomains test in linkFilter.test.js
bfe9b51 to
e52101b
Compare
There was a problem hiding this comment.
Review Summary — 3 Warnings, 1 Nitpick
The code quality is solid — previous review feedback has been largely addressed (safeReply, error isolation, domain normalization, PermissionFlagsBits, dynamic reason text, shared isExempt utility, eviction test coverage). The remaining issues are documentation gaps that violate AGENTS.md requirements.
Warnings
-
config.json — missing rateLimit and linkFilter config defaults — Per AGENTS.md: 'Config for your module goes in config.json under a new key.' Both modules read from config.moderation.rateLimit and config.moderation.linkFilter, but neither section exists in the default config. Add both sections (disabled by default) inside the moderation block after logging.
-
AGENTS.md — missing Key Files entries — Per AGENTS.md: 'Added a new module → update Key Files table.' Add entries for src/modules/rateLimit.js, src/modules/linkFilter.js, and src/utils/modExempt.js.
-
README.md — missing documentation for new config options — Per AGENTS.md: 'if you add a new config section or key, document it in README.md config reference.' The moderation config table (lines 181-201) needs rows for rateLimit.* and linkFilter.* options.
Nitpick
- src/utils/modExempt.js:23 — isExempt doesn't check bot owners or ManageGuild — The existing isModerator() in src/utils/permissions.js exempts bot owners and ManageGuild holders. isExempt doesn't, so bot owners and ManageGuild users will still be rate-limited/link-filtered.
Summary
Adds rate limiting and link filtering to the moderation pipeline.
Closes #39
Changes
Rate Limiter (
src/modules/rateLimit.js)Link Filter (
src/modules/linkFilter.js)Event Pipeline — wired before existing spam check:
rate limit → link filter → spam patterns → AI triage
Config
{ "moderation": { "rateLimit": { "enabled": true, "maxMessages": 10, "windowSeconds": 10 }, "linkFilter": { "enabled": true, "blockedDomains": ["evil.com"] } } }Testing
73 test files, 1520 tests passing (47 new: 13 rate limit + 34 link filter)