Skip to content

Make the bot self-hostable with env config and no external data sharing - #1

Merged
atharvadeosthale merged 3 commits into
mainfrom
self-host-env-config
Oct 7, 2026
Merged

atharvadeosthale merged 3 commits into
mainfrom
self-host-env-config

Conversation

@atharvadeosthale

Copy link
Copy Markdown
Member

Summary

Prepares this fork of DH-555/spam-guard-bot to run in the Appwrite Discord on a VPS, without Docker. All settings now come from environment variables, the bot no longer sends anything to the upstream author's server, and detection is tuned for a developer community.

Why

An audit of the fork found:

  • Data leaving the server. Detection feedback and !spamreport copies (message text, images, user IDs) were sent to a channel hardcoded to the upstream author's Discord server. This was on by default.
  • False positives on normal dev messages. The "remote job scam" rules timed out messages such as "I'm looking for help with my React auth flow, can someone DM me?" and "Can you help us? Phone number login fails after GitHub OAuth".
  • An unbounded !spamreport scan that paged through every channel's entire history.
  • Upstream-specific features that don't belong in an official bot: Anti-Nova & VoidBox, which deletes two third-party bots' messages with mocking audit-log reasons, and an easter-egg reply.

Changes

Data and configuration

  • The hardcoded feedback channel is gone. Feedback now goes to FEEDBACK_CHANNEL_ID, which is off when unset.
  • Removed local analytics and the /spam command.
  • Every setting is an environment variable, validated at startup with an error that names the variable. .env.example and the README document each one.
  • Removed /setup and its data/settings.json store. The bot keeps no state on disk apart from the OCR model cache.
  • MODERATION_CHANNEL_ID is required. At startup the bot checks it can post in the alert channels and exits if not. It only moderates the server that owns that channel. The public "Message deleted: @user" fallback notice is removed.
  • On startup, the bot logs each EXCLUDED_ROLE_IDS role by name and warns about IDs that don't exist in the server.

Detection

  • Text scams: only the hiring/recruitment rule remains (TEXT_SCAM_ENABLED). It now needs real hiring intent ("hiring", "looking for a developer", "join our team"), a tech role, and a contact request. Bare "looking for" and "join" no longer count. The identity, remote-career, and giveaway rules are removed.
  • NSFW and porn: the word "NSFW" is allowed. Porn-related wording is removed instead. Bare "xxx" (a placeholder) and "sex" (a schema field) are deliberately not matched.
  • Known spammers: the IDs in spam-users.json now follow SPAM_MESSAGES_ENABLED; before, they applied even with spam protection off.
  • Audit channel: removed 740463504602955806 from scam-image-channels.json. Upstream had listed it as a scam-image source, but it is Appwrite's audit channel. Tests use a made-up ID.
  • !spamreport: behind SPAM_REPORT_ENABLED (off by default), with SPAM_REPORT_LOOKBACK_MINUTES (default 60) capping the scan. Replies are in English.
  • Defaults: ANTI_RAID_LEVEL defaults to medium. .env.example ships with the two team role IDs, the audit channel, and a 60-minute launch timeout.

Fixes

  • NSFW invite check crash: it recursed forever on invites to any server with a welcome screen, which most large servers have. That aborted every later check for the message, so a scam image posted alongside such an invite was never scanned. Reproduced with real discord.js objects; regression test added.
  • Duplicate actions: Discord sends a message update when it adds a link preview. If that arrived while the bot was still handling the message, the author was timed out twice and moderators got two alerts. Messages are now handled one at a time per ID and acted on once.
  • Raid alerts: they now show whether the timeout actually succeeded.
  • Memory: the ONNX Runtime memory arena is disabled. Over 160 OCR runs, memory settles around 1.6 GB instead of 3.1 GB, at the same speed and detection rate.

Removed

Anti-Nova & VoidBox, the easter egg, Dockerfile and compose files, the GHCR deploy workflow (replaced by a test-only workflow), and the upstream privacy and terms pages.

Testing

  • pnpm test: 105 tests pass. There are new tests for config parsing, the hiring rule (7 ads caught, 8 normal messages allowed), porn wording, the update race, welcome-screen invites, !spamreport gating and lookback, and the feedback channel.
  • Ran the visual scam-image matcher over 2,816 real images, including Appwrite console screenshots: 0 false matches.
  • Ran OCR end to end on plain Node. The first start downloads about 30 MB of models from Hugging Face; memory figures are above.
  • Smoke-tested startup: missing or invalid variables fail with a clear message, and a valid config reaches the Discord login.

Before launch

Each step is in the README:

  1. In the Developer Portal, turn off Public Bot and turn on Message Content Intent.
  2. Invite the bot with the README's URL. It grants exactly the 8 permissions the bot uses.
  3. Drag the bot's role above the roles it should be able to time out.
  4. Copy .env.example to .env on the server and add DISCORD_TOKEN.
  5. Give the VPS at least 4 GB of RAM, and keep the systemd restart limit. Discord resets a bot's token after 1,000 logins in 24 hours.

Open question

The lists inherited from upstream still need a decision: spam-users.json (5 user IDs), malicious-servers.json (13 server IDs, 2 no longer exist), and zangi.com in blocked-domains.json, which is a real messaging app.

Prepare the fork to run in the Appwrite Discord on a VPS without Docker.

Data and privacy:
- Remove the hardcoded feedback channel in the upstream author's server.
  Detection feedback and !spamreport copies now go to FEEDBACK_CHANNEL_ID,
  which is off when unset.
- Remove local analytics and the /spam command.

Configuration:
- Move every setting to environment variables (src/config.js), validated at
  startup. Remove /setup and its JSON settings file; the bot keeps no state.
- Require MODERATION_CHANNEL_ID. Check at startup that the bot can post in
  the alert channels, and only moderate the server that owns the moderation
  channel. Drop the public "Message deleted" fallback notice.
- Warn at startup about EXCLUDED_ROLE_IDS that do not exist.

Detection:
- Keep only the hiring/recruitment text rule (TEXT_SCAM_ENABLED) and tighten
  it so help requests and event invites are not flagged. Remove the identity,
  remote-career, and giveaway rules.
- Allow the word "NSFW"; remove porn-related wording instead.
- Gate known spammer IDs behind SPAM_MESSAGES_ENABLED.
- Remove Appwrite's audit channel from scam-image-channels.json.
- Put !spamreport behind SPAM_REPORT_ENABLED (off) with a lookback window.

Fixes:
- Stop the NSFW invite check from recursing forever on invites to servers
  with a welcome screen, which skipped all later checks for the message.
- Act once on a message when Discord sends an update (link preview) while
  it is still being handled, instead of alerting twice.
- Show the real timeout result in raid alerts.
- Disable the ONNX Runtime memory arena: about 1.6 GB instead of 3.1 GB of
  memory after 160 images, at the same speed and accuracy.

Removed: Anti-Nova & VoidBox, the easter egg, Docker files, the GHCR deploy
workflow (replaced by a test-only workflow), and the upstream legal pages.
The README now covers setup, every setting, and running under systemd with
a restart limit that protects the bot token.
@hansi-codes

hansi-codes Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

The new systemd deployment runs the network-facing image-processing bot as root rather than a dedicated unprivileged user.

Makes the bot self-hostable with validated environment configuration, optional feedback routing, and moderation restricted to one configured server. Removes upstream data sharing and obsolete features, narrows scam detection, bounds manual reports, and fixes invite recursion and duplicate moderation actions. Adds automated VPS installation and updates, a systemd service, revised operating instructions, and shell-script checks in CI.

Latest changes: The newest commits add root-run install and update scripts with a templated systemd service, rewrite the README around that deployment workflow, and add ShellCheck to CI.

Verdict New comments Fixed Still open
✅ Approved 1 0 0
Finding Where
🟡 Run the bot service under a dedicated unprivileged user deploy/spam-guard-bot.service:14
Fix with agent prompt
### Issue 1
deploy/spam-guard-bot.service:14-17
**Run the bot service under a dedicated unprivileged user**

Without `User=`, this system service runs as root, including the native image decoders processing attachments from Discord users; `NoNewPrivileges` and `ProtectSystem=full` do not remove its existing root privileges. Please provision a dedicated service user in the installer and give it access to the token, generated manifest, and OCR cache so a compromised bot process does not also compromise the VPS as root.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
📂 Walkthrough · 23
File Change
.env.example, README.md Document environment configuration, Appwrite defaults, automated VPS setup, updates, and operating checks.
.github/workflows/test.yml, .github/workflows/deploy.yml Replace container deployment with test CI and add shell-script checking.
.dockerignore, Dockerfile, compose.production.yaml, compose.yaml Remove Docker deployment configuration.
.gitignore, data/.gitkeep Remove obsolete local-state exclusions and the data placeholder.
deploy/spam-guard-bot.service Add a templated systemd service with manifest generation and bounded restarts.
scripts/install.sh, scripts/update.sh Automate dependency installation, token setup, service installation, and fast-forward updates.
package.json Rename the package and remove the easter-egg build step.
scam-image-channels.json, spam-description-patterns.json Remove the Appwrite audit channel from scam sources and replace NSFW patterns with porn wording.
scripts/build-visual-reference-manifest.mjs Use repository-relative reference paths and the shared manifest destination.
src/config.js Define defaults and validate environment-only settings.
src/alert-channels.js, src/index.js Validate alert destination permissions, restrict moderation to one server, and remove setup registration.
src/moderation.js Use environment settings, serialize message handling, prevent duplicate actions, and report raid timeout outcomes.
src/detection.js, src/spam-messages.js Narrow recruitment detection and report matched blocked wording.
src/detection-feedback.js Route feedback only to an optional configured channel without analytics.
src/spam-report.js Gate manual reporting and configure its timeout and bounded history scan.
src/nsfw-servers.js Prevent recursion through cyclic invite metadata.
src/ocr.js Disable ONNX memory arena allocation and pattern caching.
src/i18n.js Remove obsolete feature strings and update report and feedback messages.
src/analytics.js, src/settings-store.js, src/setup-command.js, src/easter-egg-matching.js Remove analytics, persisted setup, and easter-egg implementations.
docs/*, easter-egg photos/*, scripts/build-easter-egg-photo-manifest.mjs Remove upstream legal pages, easter-egg assets, and associated build tooling.
test/alert-channels.test.js, test/config.test.js, test/detection.test.js, test/images.test.js, test/invite-protection.test.js, test/moderation.test.js, test/scam-image-channels.test.js, test/spam-messages.test.js, test/spam-report.test.js Update coverage and add regressions for configuration, detection, alert permissions, invite cycles, moderation races, and manual reports.
test/analytics.test.js, test/easter-egg-matching.test.js, test/settings-store.test.js, test/setup-command.test.js Remove tests for deleted features.
src/index.js Validates alert channels, limits moderation to their owning server, and removes setup registration.

Reviewed the commits since 33b1d5e · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread src/detection.js Resolved
Comment thread src/index.js Resolved
…issions

- Only allow role qualifiers ("a senior full-stack", "2 React Native")
  between "looking for" and a role in the recruitment rule. Help requests
  such as "looking for help from a developer ... DM me" no longer match.
- Check each alert channel for the permissions it needs at startup: Attach
  Files for the feedback channel, which receives image copies, and Send
  Messages in Threads when a channel is a thread. The error lists every
  missing permission.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Tier S · Looks good to merge. Summary

- deploy/spam-guard-bot.service: the service file, kept in the repo.
- scripts/install.sh: installs Node.js 22 and pnpm if missing, installs
  dependencies, creates .env and asks for the token (hidden), then installs
  and starts the service. Safe to rerun; clears the restart limit.
- scripts/update.sh: pulls the latest code and reruns the installer.
- README: install is now clone plus one command; adds everyday commands.
- CI: shellcheck the scripts.

Tested in a fresh Ubuntu 24.04 container: fresh install, hidden token
prompt, rerun, update, systemd-analyze verify, OCR, and bot startup.
@atharvadeosthale
atharvadeosthale merged commit 4b686c7 into main Oct 7, 2026
2 checks passed

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Tier A · Looks good to merge. Summary

Comment on lines +14 to +17
[Service]
WorkingDirectory=@BOT_DIR@
ExecStartPre=@NODE@ scripts/build-visual-reference-manifest.mjs
ExecStart=@NODE@ src/index.js

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run the bot service under a dedicated unprivileged user

Without User=, this system service runs as root, including the native image decoders processing attachments from Discord users; NoNewPrivileges and ProtectSystem=full do not remove its existing root privileges. Please provision a dedicated service user in the installer and give it access to the token, generated manifest, and OCR cache so a compromised bot process does not also compromise the VPS as root.

Prompt To Fix With AI
This is a comment left during a code review.
Path: deploy/spam-guard-bot.service
Line: 14-17

Comment:
**Run the bot service under a dedicated unprivileged user**

Without `User=`, this system service runs as root, including the native image decoders processing attachments from Discord users; `NoNewPrivileges` and `ProtectSystem=full` do not remove its existing root privileges. Please provision a dedicated service user in the installer and give it access to the token, generated manifest, and OCR cache so a compromised bot process does not also compromise the VPS as root.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · security · Reply if this doesn't apply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant