The community bot powering the official Arduino Discord
Helpful tags, image-spam moderation, and server automation — built for the people who keep the server running.
| 🏷️ Slash-command tags | Curated troubleshooting guides (/tag) — AVRDUDE errors, level shifters, powering boards, and more |
| 🛡️ Image-spam automod | Catches the image-cluster spam wave with burst + cross-channel fan-out + perceptual-hash detection, and a one-click moderator console |
| 🧰 Server automation | Auto-crosspost, join/leave + invite-source logging, role-select buttons, message-link quoting |
| 🗣️ Staff tooling | /say to broadcast rich embeds as the bot |
| 🐳 One-command deploy | docker compose up — bot + Postgres, migrations applied automatically |
git clone https://github.com/arduinodiscord/bot.git
cd bot
cp .env.example .env # add your BOT_TOKEN (+ any feature channel IDs)
docker compose up -d --build # builds the bot, starts Postgres, applies migrationsThat's it — the bot connects, registers its slash commands, and is ready.
npm install
cp .env.example .env # set BOT_TOKEN
npm run dev # rebuilds on change (tsc -w) and restarts the bot (node --watch)Privileged intents: enable Message Content and Server Members for your application in the Discord Developer Portal. Invite it with the permission set in SETUP.md (section 1), which includes a ready-made invite URL.
Everything is driven by environment variables (see .env.example).
Only BOT_TOKEN is required; every other feature self-disables until you
give it a channel or role id, so you can adopt them one at a time.
| Variable | Purpose |
|---|---|
BOT_TOKEN |
Required. Your Discord bot token |
MOD_LOG_CHANNEL_ID |
Enables the image-spam automod console |
DATABASE_URL |
Postgres connection (optional — runs in-memory without it; wired automatically by Docker) |
JOIN_LEAVE_LOG_CHANNEL_ID |
Member join/leave + invite-source logging |
CROSSPOST_CHANNEL_IDS · CROSSPOST_LOG_CHANNEL_ID |
Auto-publish announcement channels |
ROLE_SELECT_MESSAGE_ID · EVENT_NOTIFS_ROLE_ID · SERVER_UPDATE_NOTIFS_ROLE_ID |
Button-based opt-in roles |
AUTOMOD_* |
Detector thresholds & timeouts (sensible defaults; see .env.example) |
All commands are Discord slash commands — type / in the server.
| Command | Description |
|---|---|
/tag name:<tag> [user:@user] |
Post a curated troubleshooting guide, optionally pinging someone |
/solved [helper:@user] |
Mark the current help post solved (and thank a helper); closes the thread |
/openposts |
List open help posts waiting for an answer, oldest first (ephemeral) |
/about |
Bot, Node, and version info |
/ping |
Latency & uptime |
/say channel:<#ch> title:… description:… |
Staff only — send a custom embed (optional fields as name | value per line, and thumbnail) |
Plus a "Request more info" right-click (message context-menu) action that
posts the needinfo checklist to an asker in one click.
📚 Available /tag topics
ai · ask · avrdude · codeblock · debounce · espcomm · help ·
hid · lab · language · levelShifter · libmissing · needinfo ·
ninevolt · power · pullup · reinstall · wiki
A wave of spam — both freshly-joined accounts and compromised long-time members — posts clusters of images to advertise. This bot targets exactly that pattern without ever disabling image sharing server-wide.
Detection
- Burst — several image messages from one user in a short window (stricter for new members) → alerts mods.
- Cross-channel fan-out — the same image across multiple channels → high confidence (this is what catches compromised veterans, where account age tells you nothing).
- Known-spam blocklist — once a mod confirms an alert, that image is fingerprinted and future copies are caught instantly.
Each image gets a cheap metadata signature (catches identical re-uploads) and a perceptual hash (dHash from a tiny thumbnail — catches re-encoded / resized copies via Hamming distance).
Response (tiered): high-confidence hits auto-delete + timeout and then alert; bursts only alert. Every alert lands in the mod-log channel with Confirm / Timeout / Ban / Delete / Not spam buttons — a human stays in the loop. Members with Manage Messages (or a configured immune role) are never inspected.
Complements your existing YAGPDB AutoMod rather than replacing them.
Some users fragment a single thought across a stream of one-word messages
instead of sending it as one message — which buries ongoing conversation and
leaves no room to reply. The bot flags this pattern: N short messages from
one user inside a short window (AUTOMOD_FLOOD_*, defaults: 5 messages ≤ 25
chars in 15 s) lands a Message flooding alert in the same mod-log console as
the image automod, with the same Confirm / Timeout / Ban / Delete / Not-spam
buttons. By default it only alerts (flooding is usually a habit, not an attack);
set AUTOMOD_FLOOD_AUTO_TIMEOUT=true to also time the user out automatically.
Members with Manage Messages (or a configured immune role) are never flagged.
New members often fire the same question into every channel at once instead
of the one that fits. Two tenure-aware signals catch this (AUTOMOD_CROSSPOST_*):
- Near-identical fan-out (any tenure) — the same question (token-overlap ≥
SIMILARITY_PCT, default 80%) acrossCHANNELS+ channels (default 2) inside the window. High confidence, so the bot auto-deletes the duplicate copies, keeping the first, then alerts. - New-member spread (recent joiners only) — posting substantive messages in
SPREAD_CHANNELS+ channels (default 3) at once, even when reworded enough to dodge the similarity check. Alerts only — these aren't strict duplicates.
Both surface in the same mod-log console with the usual action buttons, reuse
the existing new-member window, and respect the immune-role/permission checks.
Short greetings and reactions (below MIN_CHARS) are ignored.
Ambient helpers, each self-disabling until configured:
- Auto-crosspost announcement channels, with logging.
- Join/leave logging with invite-source attribution (+ optional analytics when a DB is present).
- Role-select buttons to toggle event / server-update opt-in roles.
- Message-link flattening — quote a linked message inline for context.
Features aimed at taking repetitive load off the community members who answer the most questions:
- Keyword → tag suggestions — when a message matches a known signature
(AVRDUDE, missing-library, ESP upload, level shifters, power, pull-ups, 9V,
HID, debounce…), the bot offers the matching tag via a single button, so
askers self-serve before a helper repeats a canned answer. Each trigger lives
next to its tag in
tags.ts. Per-user cooldown; toggle withTAG_SUGGEST_ENABLED. Only fires for members with no server role — helpers, knowledgeable members, and any other role-holder are never shown suggestions. - Unformatted-code nudge — detects code pasted as plain text and offers the
codeblocktag, the single most-repeated ask. ToggleCODE_FORMAT_SUGGEST_ENABLED. Subject to the same role exemption as keyword suggestions. - "Just ask" nudge — replies to low-effort pings ("can I ask?", "anyone
here?") with the
asktag. Conservative patterns; off by default, enable withASK_SUGGEST_ENABLED=true. Subject to the same role exemption as keyword suggestions. - Suggestion ignore list — set
SUGGEST_IGNORE_CHANNEL_IDSto suppress all three suggestions in specific channels (e.g. staff channels). Listing a forum channel's ID silences all its threads. - "Request more info" context-menu — right-click any message → Request more
info to post the
needinfochecklist to the asker in one click. Helpers (REQUEST_INFO_ROLE_IDS) and staff only. - Auto-needinfo on thin posts — when a new help thread lacks substance (no
code, image, URL, or inline code, and the post title + body together are under
HELP_NEEDINFO_MIN_CHARScharacters), the bot sends a concise checklist and the Mark Solved button in a single message. Off by default (HELP_AUTO_NEEDINFO=trueto enable). The full/tag needinfochecklist used by helpers is unaffected. - Solve workflow — a Mark Solved button on new help threads (set
HELP_CHANNEL_IDS) plus/solved [helper:@user], which closes the post and credits whoever helped. - Stale-post nudge & auto-archive — abandoned help posts get a "still need
help?" nudge after a few days, then auto-archive only if no one replies for a
while longer (defaults 3 then 7 days — helpers often take a while; tune with
HELP_STALE_NUDGE_HOURS/HELP_STALE_ARCHIVE_HOURS). /openposts— an ephemeral digest of open help posts, oldest-waiting first, so helpers can pick up whatever's been waiting longest.
Help channels can be forum or text channels. List both kinds in
HELP_CHANNEL_IDS— forum posts and threads opened inside text help channels get the same Mark-Solved / needinfo / stale-sweep //openpoststreatment. The keyword/code/ask suggestions and Request more info work server-wide regardless of channel type (subject to role exemptions and ignore lists). Plain, thread-less messages in a text channel can't be archived, so the thread lifecycle simply doesn't apply to them.
src/
├── index.ts # client bootstrap (intents, presence, login)
├── commands/ # slash commands (about, ping, tag, say, solved, openposts)
├── listeners/ # gateway events (ready, messages, members, invites…)
├── interaction-handlers/ # button handlers (spam console, role-select, tag buttons)
└── utils/
├── automod/ # spam detection: tracker, flood, crosspost, phash, blocklist, console, incidents
├── config.ts # env-driven configuration
├── db.ts # optional Prisma (pg driver adapter) with in-memory fallback
├── tags.ts # tag content + schema
└── embed.ts # shared base embed
prisma/ # schema + migrations
Stack: discord.js v14 · Sapphire framework · TypeScript 6 · Prisma 7 (+ Postgres, optional).
Want to add a tag or a feature? PRs welcome!
- Fork & branch.
- Add a tag: edit
src/utils/tags.tsand add it to the.addChoices()list insrc/commands/tag.ts. npm run buildto type-check.- Open a PR against
stagingwith a clear description.
Found a bug or have an idea? Open an issue.
License: GPL-3.0-or-later · Made with ❤️ by the Arduino Discord community