Skip to content

Add image-spam automod, server management, and infrastructure#23

Draft
max-bromberg wants to merge 21 commits into
stagingfrom
revamp
Draft

Add image-spam automod, server management, and infrastructure#23
max-bromberg wants to merge 21 commits into
stagingfrom
revamp

Conversation

@max-bromberg

@max-bromberg max-bromberg commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

This is a major feature release that adds a comprehensive image-spam automod system, server-management tooling, database persistence, and Docker deployment support. The bot evolves from a simple tag-posting utility to a full-featured community moderation and automation platform.

Key Changes

Image-Spam Automod

  • Perceptual-hash and metadata-signature based detection of spam images across channels
  • Burst detection (same-channel rapid-fire) and fan-out detection (same image across channels)
  • Blocklist system with moderator-confirmed spam fingerprints persisted to Postgres
  • Auto-action on high-confidence signals (message deletion + timeout)
  • Interactive moderation console with one-click action buttons for staff review

Server Management & Automation

  • Member join/leave logging with invite-source attribution
  • Auto-crosspost (publish) messages in announcement channels
  • Role-select buttons for opt-in notification roles
  • Message-link quoting (embeds linked messages inline)
  • /say command for staff to broadcast rich embeds as the bot

Help Forum Integration

  • /solved command and button to mark help posts solved and credit helpers
  • Auto-posted "Mark Solved" button in configured help forums
  • Tag suggestion system that prompts users with relevant tags based on error patterns

Database & Persistence

  • Prisma ORM with Postgres adapter for blocklist and analytics persistence
  • Automatic migrations on startup
  • In-memory fallback when no database is configured (all features still work)
  • Member analytics (join/leave events) recorded to database

Infrastructure

  • Docker Compose setup with bundled Postgres service
  • Automatic database initialization and migration on container startup
  • Environment-driven feature flags (each feature self-disables when unconfigured)
  • Comprehensive .env.example with sensible defaults

Code Organization

  • Extracted shared embed builder to utils/embed.ts
  • New utils/automod/ module with tracker, incidents, blocklist, and console logic
  • New utils/resolveTag.ts for tag resolution and templating
  • Interaction handlers for button-based moderation and role selection
  • Listeners for message analysis, member events, and server automation

Documentation

  • Completely rewritten README with feature highlights, quick-start guides, and configuration table
  • Added badges for license, discord.js version, Sapphire framework, TypeScript, and Node.js

Notable Implementation Details

  • Perceptual hashing: Uses difference hashing (dHash) via Jimp to detect re-encoded/resized image variants within a configurable Hamming distance
  • Metadata signatures: Cheap, download-free fingerprints built from content-type, byte size, and dimensions for exact-match detection
  • Incident tracking: In-memory ephemeral store of open moderation alerts; incidents expire on bot restart (buttons gracefully report expiry)
  • Privilege checks: Automod actions and moderation buttons require ManageMessages permission; /say requires ManageGuild
  • New member heuristic: Stricter burst threshold for accounts younger than newMemberWindowMs to catch join-then-spam patterns
  • Invite attribution: Snapshots invite use counts on ready and diffs on join to determine which invite a member used
  • Feature flags: Every optional feature (MOD_LOG_CHANNEL_ID, JOIN_LEAVE_LOG_CHANNEL_ID, etc.) self-disables when unset; no configuration errors

Version

Bumped from 0.3.0 to 1.0.0 to reflect the significant feature expansion and production-readiness of the automod system.


Update — text automod + helper-assist (commits acc7ab46d809b9)

Text-based automod

  • Text-flooding detection — flags a user fragmenting one thought across many tiny messages (default 5 messages ≤ 25 chars in 15s). Alert-only by default; optional auto-timeout via AUTOMOD_FLOOD_AUTO_TIMEOUT. New utils/automod/flood.ts.
  • Cross-channel question-spam detection — two tenure-aware signals: near-identical fan-out (token-overlap similarity across N+ channels) auto-deletes duplicate copies keeping the first; new-member content-agnostic "spread" alerts only. New utils/automod/crosspost.ts.
  • Both surface in the same mod-log console with the existing action buttons.

Helper-assist (Phase 2)

  • Unified suggestion engine handles keyword→tag, unformatted-code, and low-effort-ask detection with one reply and one per-user cooldown. Tag triggers co-located in tags.ts; coverage expanded from 3 to 9 topics.
  • Auto-needinfo on thin help posts, stale-post nudge & auto-archive, /openposts ephemeral digest.
  • Help channels generalised to accept forum or text channels via HELP_CHANNEL_IDS.

Update — pHash, deployment, and rollout hardening (commits fd89c40c5cbcea)

DCT-based pHash (fd89c40)

Replaces dHash with a proper perceptual hash: 32×32 greyscale input → 2D DCT → top-left 8×8 low-frequency block compared against its own mean. Significantly more robust against re-encoding, recompression, watermarking, and minor crops — the exact transforms spammers apply to evade blocklists. Cosine table precomputed at module load; thumbnail request bumped to 64×64. Output format (16 hex chars, Hamming-comparable) and all downstream consumers are unchanged.

Docker-compose env coverage + Dockge setup guide (e80acb4)

The compose file was missing every env var added during this branch — all helper-assist, flood/crosspost automod, phash threshold, and server-management IDs. Variables not listed in the environment block are silently ignored even when present in .env, so none of the new features could be configured without this fix. Migration command switched from npx prisma to node_modules/.bin/prisma. New SETUP.md is a step-by-step Dockge deployment guide with Discord application setup, privileged-intent requirements, ID collection table, and a full env-var reference grouped by feature.

Helper-assist rollout hardening (74effe8, c5cbcea)

  • Role-based suggestion exemption — tag/code/ask suggestions are suppressed for members holding any role listed in SUGGEST_IMMUNE_ROLE_IDS (Trusted, Knowledgeable, Helper, Moderator, etc.). Self-assignable notification roles are intentionally excluded so those members still receive suggestions. Uses explicit role ID matching rather than a blanket "any role" check.
  • SUGGEST_IGNORE_CHANNEL_IDS — comma-separated channel IDs where all suggestions are suppressed (staff channels, announcements, etc.). Listing a forum channel's ID silences all its threads.
  • HELP_AUTO_NEEDINFO defaults to false — must be explicitly opted in after observing the Mark Solved rollout.
  • Tighter thin-post detection — the needinfo check now uses thread title + body combined (forum titles are often the whole question), plus four independent "has substance" signals: code block, inline backtick code, image attachment, any URL. Any one of these bypasses needinfo regardless of length.
  • HELP_NEEDINFO_MIN_CHARS default raised to 120 (combined title + body, was 60 body-only).
  • Single combined message — when auto-needinfo fires, the concise checklist and Mark Solved button are sent in one message instead of two. The full /tag needinfo content used by helpers via the context menu is unchanged.

max-bromberg and others added 21 commits June 11, 2026 22:27
Introduces a detection engine for the image-spam pattern slipping past our
filters: clustered image posts from both fresh accounts and compromised
veterans. Detection uses Discord attachment metadata only (no downloads):

- Image burst: N image messages from one user in a short window (low
  confidence -> alert moderators only).
- Cross-channel fan-out: same image across multiple channels in a window
  (high confidence) -> catches compromised veterans where account age is
  useless.
- Known-spam blocklist: moderator-confirmed image fingerprints are
  auto-actioned on sight.

Tiered enforcement: high-confidence hits auto-delete and timeout, then
alert; bursts alert only. Alerts post to MOD_LOG_CHANNEL_ID with action
buttons (confirm / timeout / ban / delete / dismiss) keeping a human in the
loop. Members with Manage Messages or a configured immune role are skipped.

Persistence is optional: the hot path is in-memory, with a Postgres-backed
blocklist that gracefully no-ops when no database is configured. Adds a
docker-compose stack (bot + Postgres), an initial Prisma migration, and
.env.example.

Also fixes the pre-existing compile errors that were blocking `npm run
build` (ping.ts async/typo, tags.ts typing, unused listener params) so the
project once again builds to a deployable state.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Low-risk ecosystem update: discord.js 14.19 -> 14.26, @sapphire/framework
5.3 -> 5.5, @sapphire/plugin-logger 4.0 -> 4.1, @sapphire/ts-config
5.0.1 -> 5.0.3. Build passes unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Prisma 7 removes the embedded query engine in favour of driver adapters and
no longer accepts the connection `url` in the schema datasource:

- Drop `url` from prisma/schema.prisma's datasource block.
- Add prisma.config.ts holding the migration/introspection connection URL,
  guarded so offline `prisma generate` (postinstall) still works when
  DATABASE_URL is unset.
- Connect the runtime client through @prisma/adapter-pg (new deps:
  @prisma/adapter-pg, pg), keeping the graceful in-memory fallback.

Build and offline/online client generation both pass.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
TypeScript 6 deprecates the (here unused) `baseUrl` compiler option, so it
is removed; all imports are relative and no `paths` mapping existed. Build
passes on the new toolchain.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
New members (joined within AUTOMOD_NEW_MEMBER_WINDOW_MS, default 72h) get a
stricter burst threshold (AUTOMOD_NEW_MEMBER_BURST_THRESHOLD, default 2) so
join-then-spam trips faster. Stays "monitor + flag" with no hard image gate.
Compromised veterans are unaffected by tenure and remain covered by the
fan-out and blocklist signals.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Each image now carries two fingerprints: the existing download-free metadata
signature (catches byte-identical re-uploads) and a perceptual dHash computed
from a tiny media-proxy thumbnail (catches re-encoded/resized copies via
Hamming-distance matching).

- New phash.ts: dHash via jimp + Hamming distance + bounded thumbnail fetch.
- Fan-out detection gains a perceptual pass alongside the exact-signature pass.
- Blocklist stores both fingerprint kinds (SpamSignature.kind) with a warm
  in-memory cache loaded on ready; perceptual matches use Hamming distance.
- AUTOMOD_PHASH_THRESHOLD tunable; docs updated.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Modernizes the legacy bot's ambient listeners to Sapphire/discord.js v14:

- Auto-crosspost configured announcement channels, with result logging
  (crosspost.ts; enabled via CROSSPOST_CHANNEL_IDS).
- Message-link flattening: inline quote embeds for same-guild message links
  (messageLinkEmbed.ts).
- Join logging with invite-source attribution via an in-memory invite-use
  cache seeded on ready and kept current by an inviteCreate listener
  (memberAdd.ts, inviteCreate.ts, ready.ts, utils/inviteCache.ts). Adds the
  GuildInvites intent.
- Leave logging (an improvement over legacy, which logged joins only).
- Best-effort join/leave analytics via the existing MemberAnalytics model.

Each feature self-disables when its channel/role id is unconfigured. The
legacy file-extension filter is intentionally dropped in favour of Discord's
native AutoMod.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
- Role-select interaction handler toggles the event / server-update opt-in
  roles when their buttons are clicked on the configured role-select message
  (roleSelect.ts; gated by ROLE_SELECT_MESSAGE_ID + role ids).
- Per-tag buttons: a `tag:<name>` button replies ephemerally with that tag,
  restoring the legacy "codeblock" button on the `ask` tag. Adds a shared
  resolveTag() helper (tagButton.ts, utils/resolveTag.ts).
- /say: a Manage-Server-gated slash command that sends a custom embed
  (title, description, optional newline-delimited fields and thumbnail) to a
  chosen channel, replacing the legacy multi-prompt say flow.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
- Fix the Dockerfile: drop `apk add openssl1.1-compat` (removed from current
  Alpine and unneeded with Prisma 7's driver adapter — it was breaking the
  image build), and restructure COPY/install for proper layer caching.
- Refactor /tag to use the shared resolveTag() helper: drops the `any`
  payload, modernizes `ephemeral` -> MessageFlags, uses isSendable(), and
  fixes a gap where the in-channel path ignored embeds.
- Modernize /ping to the non-deprecated deferReply({ withResponse }) API and
  tidy formatting.
- README: document /ping, /say, the full tag list, and the ported
  server-management features.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Incorporates the language work from PR #22 (already on staging) plus the
contributor's follow-up: per real-world feedback that the old bootloader can
work on non-Nano 328P "franken-boards", the avrdude step-8 advice no longer
restricts the old-bootloader suggestion to Nanos only.

Also restores the differentiated /tag confirmation titles and sweeps the tag
content for obvious spelling/grammar errors (useing, untill, compileing,
secconds, backwords, Attemppting, reconized, repibably, commonunicate,
consistantly, reagulator, usefull, there->their, "ai"->"AI", etc.).

Co-authored-by: tuulikauri <170147490+tuulikauri@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
The shared `universalEmbed` was exported from src/index.ts, forcing every
command and listener to import the bootstrap module just to reuse an embed.
Move it to its own utils/embed.ts and repoint all importers. Also modernize
index.ts: use the ActivityType enum instead of the magic number 3, drop the
dead db import comment, and remove the now-unnecessary EmbedBuilder import.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
- Bump the bot to its first major version (0.3.0 -> 1.0.0).
- Replace the README with a reorganized, badged overview: highlights,
  one-command Docker quick start, a configuration table, commands, the
  automod and server-management features, and a project-structure map.
- Point repository/bugs/homepage in package.json at arduinodiscord/bot
  (they still referenced a personal fork).

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Three features aimed at reducing repetitive load on community helpers:

- Keyword -> tag auto-suggest (tagSuggest.ts): high-precision signatures for
  the most-repeated questions (AVRDUDE, missing library, ESP upload) surface
  the matching tag via a single button, with a per-user cooldown. Toggle with
  TAG_SUGGEST_ENABLED.
- "Request more info" message context-menu command (requestInfo.ts): one-click
  posting of the needinfo checklist to an asker.
- Solve workflow: /solved [helper] command, a "Mark Solved" button on new
  help-forum posts (threadCreate.ts + solvedButton.ts, gated by
  HELP_FORUM_CHANNEL_IDS), and shared solveThread.ts logic that titles the post
  with a checkmark, credits the helper, and archives it. OP or staff only.

Docs and .env.example updated.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01SgtVVoWBx7FRzWdGoUUE1K
Flag users who fragment one thought across many tiny messages in quick
succession, which buries conversation. Counts short messages from a user
within a sliding window and raises a "Message flooding" incident in the
existing mod-log console, reusing the same action buttons.

- New flood tracker (utils/automod/flood.ts) with per-user windowed counts,
  alert cooldown, and a self-pruning sweep, mirroring the image tracker.
- Generalise IncidentLevel to include 'flood'; console renders a level-aware
  title and auto-action note.
- Wire detection into the message listener, independent of the image automod;
  optional auto-timeout via AUTOMOD_FLOOD_AUTO_TIMEOUT (alert-only by default).
- Clear flood state on confirm/ban/dismiss; tidy the confirm summary when an
  incident carries no image fingerprints.
- Config tunables (AUTOMOD_FLOOD_*), .env.example, and README docs.
Catch new users fanning the same question across many channels at once,
which is noise and bypasses the right help channel. Two tenure-aware
signals feed the existing mod-log console:

- Near-identical fan-out (any tenure): the same question (token-overlap
  Jaccard >= threshold) across N+ distinct channels in the window. High
  confidence, so the duplicate copies are auto-deleted (first kept), then
  mods are alerted.
- New-member spread: a recent joiner posting substantive messages across
  N+ channels even when reworded past the similarity check. Alert-only,
  since these aren't strict duplicates.

- New crosspost tracker (utils/automod/crosspost.ts): per-user windowed
  buffer, token-set similarity, clustering, alert cooldown, self-pruning
  sweep — mirrors the flood/image trackers.
- Extend IncidentLevel with 'crosspost'; console renders a level-aware
  title, color, and auto-action note.
- Wire into the message listener; factor out a shared isNewMember helper
  reused by the image path. Clear crosspost state on resolve.
- Config tunables (AUTOMOD_CROSSPOST_*), .env.example, and README docs.
Builds on the Phase 1 helper-assist set to cut the most-repeated asks and
close the loop on help posts.

Repeated-ask deflectors (one unified suggestion engine, one reply, one
per-user cooldown, each individually toggleable):
- Co-locate keyword->tag triggers in tags.ts via a new `suggest` field and
  broaden coverage (level shifter, power, pull-up, 9V, HID, debounce, plus
  the original AVRDUDE/libmissing/ESP). tagSuggest now builds its signatures
  from the tags themselves.
- Detect code pasted as plain text and offer the codeblock tag
  (CODE_FORMAT_SUGGEST_ENABLED).
- Nudge low-effort "can I ask?"/"anyone here?" pings toward the ask tag
  (ASK_SUGGEST_ENABLED), with conservative anchored patterns.

Close-the-loop on help forums:
- Auto-post the needinfo checklist when a new help post is too thin
  (no code/image, short), via threadCreate (HELP_AUTO_NEEDINFO).
- Stale-post sweep: nudge open posts idle past a threshold, then auto-archive
  if no human replies after the nudge; re-engagement resets the state. Runs
  from the ready listener on an unref'd interval.
- /openposts: ephemeral digest of open help posts, oldest activity first.

Shared open-post scan (utils/helpPosts.ts) backs both the sweep and the
digest. Config knobs (HELP_*), .env.example, and README updated.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01G6Q9Af9u4QrPS93f4kgsRJ
- Raise the stale-post defaults so slow helper response doesn't archive live
  posts: nudge after 72h (was 24h), auto-archive after a further 168h of no
  human reply (was 72h). Both still env-tunable.
- Generalise help-channel handling so entries may be forum OR regular text
  channels. The thread lifecycle (Mark Solved button, auto-needinfo, stale
  nudge/auto-archive, /openposts) keys on a thread's parent being a configured
  help channel, so threads opened inside text help channels now get the same
  treatment as forum posts.
- Add HELP_CHANNEL_IDS as the preferred config, merged with (and superseding
  the naming of) the legacy HELP_FORUM_CHANNEL_IDS, which still works.
- Update wording/docs (config, .env.example, README) to reflect both channel
  types; note that message-level assists already work server-wide and that
  thread-less text messages have no archivable lifecycle.

Co-authored-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_01G6Q9Af9u4QrPS93f4kgsRJ
Swap the perceptual hash algorithm from dHash (pixel-brightness
differences) to pHash (2-D DCT on a 32×32 greyscale thumbnail, top-left
8×8 low-frequency block compared against its mean). pHash operates in the
frequency domain and is significantly more robust against re-encoding,
recompression, watermarking, and minor crops — the exact transformations
spammers use to evade exact-hash blocklists.

The output format (16 hex chars, 64-bit Hamming-comparable hash) and all
downstream consumers (tracker, blocklist, incidents, DB schema) are
unchanged. Cosine values are precomputed at module load so per-image cost
is multiplications only. Thumbnail request bumped to 64×64 so Discord's
proxy handles the expensive initial downscale before Jimp resizes to 32×32.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docker-compose.yml was missing every env var added during the revamp
branch (all helper-assist, flood/crosspost automod, phash threshold,
server-management IDs). Variables omitted from the compose environment
block are silently ignored even when present in .env, so none of the new
features could be configured without this fix. Also switches the migration
command from `npx prisma` to `node_modules/.bin/prisma` to avoid a
network lookup in the container.

SETUP.md is a step-by-step Dockge deployment guide covering Discord
application setup, privileged intents, ID collection, cloning to
/opt/stacks, .env configuration, and a full env-var reference table
grouped by feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tag/code/ask suggestions:
- Never fire for members with any server role (Trusted and above). Since
  all roles are granted manually, role presence reliably identifies
  recognised community members who don't need suggestions.
- Add SUGGEST_IGNORE_CHANNEL_IDS (comma-separated) to suppress suggestions
  in specific channels or entire forum hierarchies (staff channels, etc.).

Auto-needinfo (HELP_AUTO_NEEDINFO):
- Default flipped to false. Must be explicitly opted in.
- Thin-post check now includes the thread title in the character count,
  adds URL and inline-code detection as substantive-content signals, and
  raises the combined threshold to 120 chars. Any of code block, inline
  code, attachment, or URL bypasses needinfo regardless of length.
- When needinfo fires, the checklist and Mark Solved button are sent as a
  single message (was two). The checklist text is a short inline version;
  the full /tag needinfo content used by helpers is unchanged.

HELP_NEEDINFO_MIN_CHARS default raised from 60 (body only) to 120 (title +
body combined) to reflect the broader check.

docker-compose, .env.example, README, and SETUP updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…S list

The previous check (roles.cache.size > 1) exempted any member with any
role, including self-assignable notification/update roles. Replace it with
an explicit SUGGEST_IMMUNE_ROLE_IDS config (comma-separated role IDs) so
only the intended roles — Trusted, Knowledgeable, Helper, Moderator, etc.
— suppress suggestions, while members with only self-assignable roles
continue to receive them.

If SUGGEST_IMMUNE_ROLE_IDS is unset, no one is exempted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants