Shaw is a small Discord spoiler bot I originally built for a Silksong community.
The first version sounded pretty simple: keep a list of spoiler words and delete messages that contain them.
Then the false positives started.
A word should be allowed inside ||Discord spoiler tags||. It should not be detected because it appears inside a URL. Markdown links caused another problem because the visible text and the URL do not behave the same way. I ended up spending more time deciding when not to flag a word than detecting the word itself.
That became the interesting part of the project.
Shaw watches selected Discord channels for configured spoiler terms.
When it finds an unhidden spoiler, it:
- deletes the message;
- sends the user a temporary warning;
- logs the detected term for staff;
- increments the user's warning count; and
- sends an extra staff log when a configured warning threshold is reached.
Users with whitelisted roles can be ignored, and staff have slash commands for checking warning counts and correcting them when needed.
A normal word filter can check whether a message contains a banned word. Shaw has to care about where the match appears in the message.
Before checking spoiler terms, the bot finds several protected parts of the text:
- Discord spoiler spans:
||like this|| - URLs
- the visible text inside Markdown links
Each configured spoiler term is compiled into a case-insensitive regular expression. When the bot finds a match, it checks the start and end positions of that match against the protected spans.
If the match falls inside one of them, Shaw ignores it.
In simplified form:
message
|
+--> find Discord spoiler spans
|
+--> find URL spans
|
+--> find Markdown link-text spans
|
v
search for configured spoiler terms
|
v
is the match inside a protected span?
|
+--> yes: ignore it
|
+--> no: flag the message
This means a term can be flagged in normal text while still being allowed in situations where deleting the message would be a false positive.
For example:
Lace
would be detected if Lace is configured as a spoiler term.
But:
||Lace||
is already hidden by Discord and should be allowed.
The same idea applies to matches that only appear as part of a URL or protected Markdown text.
Shaw keeps a warning count for each user in a local JSON file.
Count changes use an asynchronous lock so two updates do not modify the in-memory warning state at the same time.
This is deliberately simple persistence. Shaw was built for one server and does not use a database. Warning counts survive normal restarts as long as the persistence file remains available, but they can be lost if the deployment environment clears local storage.
| Command | What it does |
|---|---|
/top-spoilers |
Shows users with the highest spoiler warning counts |
/spoiler-check |
Checks one user's current warning count |
/clear-user |
Lets authorized staff reset or override a warning count |
/clear-user requires a configured staff role and asks the staff member to confirm the change before applying it. Confirmed changes are written to the moderation log.
Most server-specific settings are currently stored near the top of main.py.
| Setting | Purpose |
|---|---|
ALLOWED_CHANNEL_IDS |
Channels checked for spoilers |
ALLOWED_STAFF |
Staff roles allowed to use moderation commands |
WHITELIST_ROLE_IDS |
Roles ignored by the spoiler checker |
LOG_CHANNEL_ID |
Channel used for warning and audit logs |
WARN_THRESHOLD |
Warning count that triggers an additional staff log |
AUTO_DELETE_BOT_REPLY_SECONDS |
How long temporary warning messages remain visible |
PERSISTENCE_FILE |
Local JSON file used for warning counts |
The spoiler vocabulary is also currently stored directly in main.py.
Shaw requires Python 3.10 or newer.
Install the dependencies:
pip install -r requirements.txtSet the Discord bot token:
export BOT_TOKEN="your bot token here"On Windows PowerShell:
$env:BOT_TOKEN="your bot token here"Then run:
python main.pyThe Discord application needs Message Content Intent enabled and enough server permissions to read and delete messages, send warnings, use slash commands, and post moderation logs.
Shaw is a small server-specific project, not a general moderation framework.
The main limitations are intentional or architectural:
- server configuration is hardcoded in
main.py; - the spoiler vocabulary is stored directly in the source;
- warning counts use local JSON persistence instead of a database; and
- matching, Discord events, commands, and persistence currently live in one Python module.
If I continued developing Shaw, the first things I would separate are the spoiler matcher and persistence layer. The matcher is the part I would especially want isolated and tested because most of the interesting edge cases happen there.
This is a repost of an older project from my previous GitHub account.
It is a much smaller bot than projects such as Avenue Guard or Nauta, but I kept it public because I like the problem it ended up solving. What started as a basic word filter became mostly an exercise in avoiding bad matches.
Shaw is available under the MIT License.