- TypeScript 88.1%
- JavaScript 6.1%
- Nix 5.8%
| scripts | ||
| src | ||
| .envrc | ||
| .gitignore | ||
| .nvmrc | ||
| .prettierrc | ||
| devvit.json | ||
| eslint.config.js | ||
| flake.lock | ||
| flake.nix | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
rlfriendbot
rlfriendbot helps moderate subreddits where people post to find teammates or recruit for gaming communities. It does three things: it spots repeat recruitment posts from the same author and reports them to the modqueue for a human to judge, it mirrors moderator activity to the mod team's Discord, and it flags heavily-reported content.
Who it is for: moderators of "looking for group" and community-recruitment subreddits, where the same organisers post repeatedly and the problem is noticing it, not deciding what to do about it.
Built for r/RocketLeagueFriends, replacing a self-hosted bot, but it is configured entirely from the app's settings page and nothing about it is specific to that subreddit.
Critical operational notes
Read these before installing.
-
This app never removes or bans anything. The strongest thing it does is report a post to your modqueue, which is exactly what a normal user report does. Every decision stays with a human moderator.
-
Every feature is off by default. A fresh install does nothing until you switch something on. This lets you run it alongside an existing bot and compare before handing over.
-
Discord is optional and send-only. The app posts to Discord webhooks you supply. It cannot read Discord, and there is no Discord bot to add to your server. With no webhook configured it still works; it just stays quiet.
-
The keyword list is pre-filled with r/RocketLeagueFriends' list, which includes specific community names and usernames that mean nothing on another subreddit. Replace it with your own before switching tracking on.
-
Moderator menu actions only appear in official Reddit apps and on new Reddit. Moderators on old.reddit.com with Toolbox will not see them.
What it does
Each feature is independent and separately switchable.
Repeat recruitment post tracking
Community organisers tend to post the same recruitment pitch over and over. This watches new posts for keywords you configure — community names, words like "recruit" or "members", or specific usernames — and counts how many an author makes in a rolling window, 14 days by default.
Past the threshold it reports the post to your modqueue, with a note saying how long ago that author first posted. It also posts to Discord with the keywords that matched, so the mod team can see why.
Nothing is removed. The report simply puts it in front of a moderator.
Counts expire on their own at the end of the window. Moderators can check or clear anyone's count from the Community post count menu action — useful when someone was flagged unfairly.
Mod log to Discord
Every post removal, comment removal, and ban is posted to your Discord mod log channel with the moderator's name, the content, and a link. Moderators or bots you list are skipped.
Report escalation
When a post or comment reaches a configured number of reports, it is posted to Discord. At higher thresholds it pings a moderator role. The defaults alert at 3, 5, and every count above 8, pinging at 8 and 13.
Setup
1. Install and open the settings
Install the app, then open its settings page for your subreddit (Mod Tools → Apps → rlfriendbot). Nothing happens until you configure it.
2. Connect Discord (do this first)
In Discord, open the channel you want moderation logs in, then Edit Channel → Integrations → Webhooks → New Webhook → Copy Webhook URL.
Paste it into Discord → Mod log webhook URL. Treat this URL as a password — anyone holding it can post to that channel.
| Setting | What it does |
|---|---|
| Mod log webhook URL | Where removals, bans, and reports go. Leave empty to disable all Discord output. |
| Error log webhook URL | Where the app reports its own failures. Falls back to the mod log. |
| Mod role ID to ping | Numeric Discord role ID pinged on high report counts. Leave empty to never ping. |
To get a role ID: enable Developer Mode in Discord's Advanced settings, then right-click the role and choose Copy ID.
3. Turn on the read-only features
Both only observe and report, so they are safe to enable immediately.
| Group | Setting | Notes |
|---|---|---|
| Mod log → Discord | Log removals and bans to Discord | Add usernames under Moderators to ignore to skip other bots |
| Report escalation | Alert on reported posts and comments | Thresholds and ping counts are comma-separated lists |
4. Turn on recruitment tracking
Replace the keyword list first. The default is r/RocketLeagueFriends' own list and will not make sense elsewhere.
| Setting | What it does |
|---|---|
| Track and report repeat recruitment posts | Master switch |
| Report once an author has posted more than this many times | 1 reports the author's second post in the window |
| Tracking window in days | Default 14 |
| Keywords | One per line |
How keywords match:
- A keyword matches if it appears anywhere in the post's title or body.
recruitmatches "recruiting now" and "we recruit weekly". - A keyword also matches if it is exactly the author's username. This is for
flagging specific known accounts. It is an exact match on purpose — otherwise
a keyword like
groupwould flag anyone namedgrouped_up. - Matching ignores capitalisation on both sides.
Leaving the list empty falls back to the built-in list rather than tracking nothing, so an install cannot silently do nothing.
Start with the tracking on and the report threshold set high, watch the Discord channel for a few days, and tune the keywords before lowering it.
Moderator actions
Long-press a post or comment in the Reddit app, or use the subreddit menu.
| Action | Where | What it does |
|---|---|---|
| Community post count | post, comment, subreddit | Show or clear an author's tracked post count |
Opened from a post or comment, the username is filled in for you. The field stays editable so you can look someone else up.
Forms must be completed within 10 minutes of opening — a Reddit security limit.
What it stores
Only counters, with automatic expiry. There is no external database and nothing is shared between subreddits.
| Data | Kept for |
|---|---|
| Per-author recruitment post counts | The tracking window, 14 days by default, from the author's first post in it |
| Which posts have been counted | The window plus one hour |
| Duplicate-delivery guards | 24 hours |
Post and comment content is sent to your Discord webhook when the relevant feature is on. That is the only place data leaves Reddit.
Outbound network access
| Domain | Why |
|---|---|
discord.com |
Posting moderation notifications to Discord webhooks. Send-only — never read. |
discord.com is on Devvit's global fetch allowlist, so no approval is needed.
The app sends no analytics and contacts no other host.
Troubleshooting
Nothing appears in Discord. Check the mod log webhook URL is set, and that at least one feature is switched on — everything defaults to off.
Nothing is ever reported. Either tracking is off, the keyword list does not match how people actually word posts on your subreddit, or the report threshold is higher than anyone has reached in the window.
Too much is reported. Broad keywords like group and discord catch a lot
of ordinary posts. Narrow the list, or raise the report threshold.
A moderator cannot see the menu action. They are on old Reddit or a third-party app. Menu actions render only in official Reddit clients.
For developers
App: https://developers.reddit.com/apps/rlfriendbot
Node 26 and pnpm, both supplied by the flake:
nix develop # node 26.x + pnpm 11.x on PATH
pnpm install
pnpm run login # devvit login, one time
pnpm run check # typecheck, lint, validate devvit.json, unit tests
pnpm run dev # devvit playtest against the subreddit in devvit.json `dev`
Playtesting targets r/rlfriendbot_dev (set in devvit.json under
dev.subreddit). Nothing points at r/RocketLeagueFriends until the cutover
phase — and devvit upload alone cannot get you there: an unpublished app only
installs on subreddits with fewer than 200 subscribers. Reaching a real
subreddit needs devvit publish and app review.
Layout
src/
index.ts Hono server; mounts /internal/{triggers,menu,form}
routes/ HTTP entrypoints named in devvit.json
core/ settings, Discord webhooks, Redis helpers
features/ one module per behaviour
Every endpoint in routes/ must have a matching entry in devvit.json —
triggers under triggers, menu actions under menu.items, and form handlers
under forms.
Pure logic lives in Devvit-free modules (community-match.ts,
report-thresholds.ts, expiry.ts) so it can be unit tested outside the
runtime; the Devvit-importing module wraps it.
Design notes
- No database. Counters are Redis keys whose TTL is the tracking window, so nothing has to sweep expired records and the author's first post in the window is derivable from when the key expires.
- Triggers are not exactly-once. A per-post marker key means a redelivered
onPostSubmitcannot inflate an author's count. - Discord is outbound-only — incoming webhooks, no bot token, nothing read back.
redis.expireTime()returns seconds remaining, not the absolute timestamp its docstring promises.core/expiry.tsdecides from magnitude, so it stays correct if that is ever fixed.- A self post's
urlis a permalink path, not an absolute URL — it has to be expanded before it is any use as a link. onModActioncarries no bandetails, so the reason is read back offgetModerationLog.
Before adding anything flair-dependent
Nothing here reads post or user flair today. If that changes, check whether
r/RocketLeagueFriends requires a flair at submit time: where it does not,
onPostSubmit fires before the flair exists and an onPostFlairUpdate handler
is needed as well.