Add image-spam automod, server management, and infrastructure#23
Draft
max-bromberg wants to merge 21 commits into
Draft
Add image-spam automod, server management, and infrastructure#23max-bromberg wants to merge 21 commits into
max-bromberg wants to merge 21 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Server Management & Automation
/saycommand for staff to broadcast rich embeds as the botHelp Forum Integration
/solvedcommand and button to mark help posts solved and credit helpersDatabase & Persistence
Infrastructure
.env.examplewith sensible defaultsCode Organization
utils/embed.tsutils/automod/module with tracker, incidents, blocklist, and console logicutils/resolveTag.tsfor tag resolution and templatingDocumentation
Notable Implementation Details
ManageMessagespermission;/sayrequiresManageGuildnewMemberWindowMsto catch join-then-spam patternsVersion
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
acc7ab4–6d809b9)Text-based automod
AUTOMOD_FLOOD_AUTO_TIMEOUT. Newutils/automod/flood.ts.utils/automod/crosspost.ts.Helper-assist (Phase 2)
tags.ts; coverage expanded from 3 to 9 topics./openpostsephemeral digest.HELP_CHANNEL_IDS.Update — pHash, deployment, and rollout hardening (commits
fd89c40–c5cbcea)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
environmentblock are silently ignored even when present in.env, so none of the new features could be configured without this fix. Migration command switched fromnpx prismatonode_modules/.bin/prisma. NewSETUP.mdis 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)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_NEEDINFOdefaults tofalse— must be explicitly opted in after observing the Mark Solved rollout.HELP_NEEDINFO_MIN_CHARSdefault raised to 120 (combined title + body, was 60 body-only)./tag needinfocontent used by helpers via the context menu is unchanged.