Discord bot scripts for posting novel feed updates, comments, new novel launches, new arcs, extras, and completion announcements.
This repo is the general Discord announcement layer. It reads generated RSS feeds from rss-feed, imports shared novel metadata from novel_mappings.py, then posts styled messages to Discord using the Discord bot API.
The old single-webhook setup is legacy. Current scripts use:
DISCORD_BOT_TOKEN
channel IDs from GitHub secrets/config
This repo handles:
-
Free chapter announcements
- Posts new public/free chapters.
-
Paid/advance chapter announcements
- Posts new paid/locked/advance chapters.
-
Comment announcements
- Posts new comments from hosting sites.
- Supports Novel Updates comment styling through the comments feed.
-
New novel launch alerts
- Detects first public drops such as
Chapter 1,Ch. 1,Episode 1,Ep. 1,1.1, orPrologue.
- Detects first public drops such as
-
New arc alerts
- Detects new locked/advance arcs using per-novel arc history.
-
Side story / extra alerts
- Detects when extras or side stories begin.
-
Completion announcements
- Announces when the final paid chapter appears.
- Announces when the full series unlocks for free.
- Supports only-free completion paths.
This repo no longer owns NU weekly reader reports. Those live in rss-feed.
Important files and folders:
discord-webhook/
├─ .github/workflows/
│ ├─ chapters_discord.yml
│ ├─ comments_discord.yml
│ ├─ rss_to_discord.yml
│ └─ fix-embed-other-server.yml
├─ config/
│ ├─ embeds.json
│ ├─ feeds.json
│ ├─ files.json
│ ├─ novel_discord_map.toml
│ ├─ roles.json
│ └─ tag_roles.json
├─ arc_history/
│ ├─ amlwc_history.json
│ ├─ hiaflg_history.json
│ ├─ tdlbkgc_history.json
│ └─ tvitpa_history.json
├─ message_templates/
│ ├─ comments.toml
│ ├─ completed_novels.toml
│ ├─ free_chapters.toml
│ ├─ new_arcs.toml
│ ├─ new_extras.toml
│ ├─ new_novels.toml
│ └─ paid_chapters.toml
├─ requirements/
│ ├─ chapters.txt
│ ├─ comments.txt
│ └─ rss_dispatch.txt
├─ bot_free_chapters.py
├─ bot_paid_chapters.py
├─ bot_comments.py
├─ new_novel_checker.py
├─ new_arc_checker.py
├─ new_extra_checker.py
├─ completed_novel_checker.py
├─ config_loader.py
├─ message_context.py
├─ message_renderer.py
├─ state.json
├─ state_rss.json
├─ README.md
├─ PRIVACY.md
└─ TERMS.md
Novel metadata comes from the rss-feed repo.
rss-feed stores editable novel data in split TOML files:
rss-feed/
├─ novel_mappings.py
└─ mappings/
├─ output_feeds.toml
├─ hosts/
│ └─ mistmint_haven.toml
└─ novels/
├─ tvitpa.toml
├─ tdlbkgc.toml
├─ amlwc.toml
└─ ...
Even though the data is split into TOML files, this repo still imports:
from novel_mappings import HOSTING_SITE_DATAnovel_mappings.py remains the compatibility front door.
The GitHub Actions workflows should install the latest rss-feed package:
pip install --upgrade git+https://github.com/Cannibal-Turtle/rss-feed.git@mainThis lets scripts import:
from novel_mappings import HOSTING_SITE_DATAand helper functions such as:
get_novel_details_by_short_code(short_code)
find_novel_by_short_code(short_code)
short_code_has_free_chapters(short_code)
short_code_has_paid_chapters(short_code)
short_code_has_comments_feed(short_code)The installed package provides shared mappings, not this repo’s Discord configs.
Use the requirement files in requirements/ from the workflows.
Typical install commands are:
pip install -r requirements/chapters.txt
pip install -r requirements/comments.txt
pip install -r requirements/rss_dispatch.txt
pip install --upgrade git+https://github.com/Cannibal-Turtle/rss-feed.git@mainCommon direct dependencies include:
pip install discord.py feedparser python-dateutil aiohttp requests tomlitomli is only needed for Python versions below 3.11, but it is safe to include.
Add these in:
Settings → Secrets and variables → Actions
| Secret | Purpose |
|---|---|
DISCORD_BOT_TOKEN |
Discord bot token |
PAT_GITHUB |
Optional Personal Access Token for the free chapter → rss-feed status card update callback |
Go to:
Settings → Actions → General
Under workflow permissions, enable:
Read and write permissions
Also allow actions and reusable workflows.
A PAT may still be needed for cross-repo dispatch.
Central paths used by scripts:
{
"state_path": "state.json",
"rss_state_path": "state_rss.json",
"nu_readers_path": "nu_readers.json",
"novel_discord_map_file": "config/novel_discord_map.toml",
"tag_role_map_file": "config/tag_roles.json",
"arc_history_dir": "arc_history"
}state_path is for legacy/general state.
rss_state_path is for RSS dedupe keys such as:
free_seen_guids
paid_seen_guids
comments_seen_guids
last_post_time_free
last_post_time_paid
last_post_time_comments
arc_history_dir stores per-novel arc tracking JSON.
Defines RSS source URLs and state keys:
{
"free": {
"url": "https://raw.githubusercontent.com/Cannibal-Turtle/rss-feed/main/free_chapters_feed.xml",
"last_guid_key": "free_last_guid",
"seen_key": "free_seen_guids",
"last_post_time_key": "last_post_time_free"
},
"paid": {
"url": "https://raw.githubusercontent.com/Cannibal-Turtle/rss-feed/main/paid_chapters_feed.xml",
"last_guid_key": "paid_last_guid",
"seen_key": "paid_seen_guids",
"last_post_time_key": "last_post_time_paid"
},
"comments": {
"url": "https://raw.githubusercontent.com/Cannibal-Turtle/rss-feed/main/aggregated_comments_feed.xml",
"last_guid_key": "comments_last_guid",
"seen_key": "comments_seen_guids",
"last_post_time_key": "last_post_time_comments"
},
"seen_cap": 500,
"time_backstop": true
}seen_cap limits how many GUIDs are kept per feed.
time_backstop helps prevent old items from reposting after state resets.
Server/channel behavior lives here. translator_url is optional and can be omitted.
{
"guild_id": "1329384099609051136",
"free_chapters": "1329384438542499892",
"paid_chapters": "1342475922581884968",
"comments": "1361685556526055586",
"announcements": "1330049962129489930",
"mod": "1329655743799889962",
"novel_cards_archive": "1463476725253144751",
"announce_first_arc_release": true,
"announce_first_chapter_release": true,
"include_novel_updates_comments": true
}Optional server-level fallback if you want the translator/profile link to be clickable even when it is not available from RSS or rss-feed mappings:
{
"translator_url": "https://www.mistminthaven.com/account/@CannibalTurtle-5082"
}Translator/profile URL lookup order is RSS translator_url, then rss-feed mapping translator_url, then optional config/server.json translator_url, then empty string.
Global Discord role IDs:
{
"free_global": "1342483851338846288",
"paid_global": "1342484466043453511",
"new": "1329502873503006842",
"ongoing": "1329502951764525187",
"complete": "1329502614110474270",
"nsfw": "1343352825811439616",
"admin": "1329392448798982214"
}Scripts convert IDs into mentions with:
role_id_to_mention(role_id)Keep IDs as raw numbers in JSON, not <@&...> strings.
Discord-only per-novel data lives here.
Example:
[AMLWC]
role_id = "1517842780003635240"
custom_emoji = "<:ghostcat:1517845090779791490>"
role_url = "https://discord.com/channels/.../..."This file should contain:
| Field | Purpose |
|---|---|
role_id |
Novel role ID used for pings |
custom_emoji |
Novel emoji used in display text |
role_url |
Link to the role-selection message or channel |
Do not put title, host, feed flags, cover image, NSFW, membership, or chapter metadata here. Those belong in rss-feed/mappings/novels/*.toml.
Tag-to-role map used for new novel announcements.
Keys should be lowercase/normalized tag names:
{
"quick transmigration": "1329427832077684736",
"infinite flow": "1329428382089347102",
"comedy": "1330469306936328286",
"bl": "1330469077784727562"
}Use this for language, genre, and content tags that should ping.
Embed appearance settings:
{
"colors": {
"free_chapter": "FFF9BF",
"paid_chapter": "A87676",
"comments": "F0C7A4",
"novel_updates_comments": "2D3F51",
"new_novel": "AEC6CF",
"arc_unlocked": "FFF9BF",
"arc_locked": "A87676"
}
}Colors can be:
"FFF9BF"or:
"#FFF9BF"Some message templates may also use:
color = { key = "paid_chapter", default = "A87676" }or, where supported by Python:
"paid_chapter": "novel""novel" means the script resolves the color from the novel TOML in rss-feed, usually theme_color or discord_color.
Put novel metadata:
host = "Mistmint Haven"
title = "After the Male Leads Went Crazy, They All Turned Into Male Ghosts"
short_code = "AMLWC"
novel_url = "https://..."
featured_image = "https://..."
has_free = true
has_paid = true
has_comments = true
is_nsfw = false
is_membership = false
chapter_count = "93 Chapters"
last_chapter = "Chapter 93"
start_date = "2026-..."
history_file = "arc_history/amlwc_history.json"
discord_color = "#c90016"Put Discord routing/display data:
[AMLWC]
role_id = "..."
custom_emoji = "<:...:...>"
role_url = "https://discord.com/channels/..."| Script | Purpose |
|---|---|
bot_free_chapters.py |
Reads free RSS feed and posts free chapter announcements |
bot_paid_chapters.py |
Reads paid RSS feed and posts paid/advance chapter announcements |
bot_comments.py |
Reads comments RSS feed and posts comment announcements |
new_novel_checker.py |
Detects first public chapter/new novel launch |
new_arc_checker.py |
Detects new advance/locked arcs |
new_extra_checker.py |
Detects side stories/extras |
completed_novel_checker.py |
Detects paid/free/only-free completion announcements |
All scripts share helpers from:
config_loader.py
message_context.py
message_renderer.py
The Discord scripts can use these values from the RSS item context:
title
volume
chapter
chaptername
link
description
category
translator
short_code
featured_image_url
pub_date
pub_date_iso
host
host_logo_url
guid
guid_is_permalink
Templates can reference them as:
content = "New chapter for {title}"
description = "{chaptername}"
timestamp = "{pub_date_iso}"Templates live in:
message_templates/
A basic template:
mode = "classic"
content = "{chapter_mention} New chapter for **{title}**"
[allowed_mentions]
parse = ["roles"]
[[embeds]]
title = "{chapter}"
url = "{link}"
description = "{chaptername}"
timestamp = "{pub_date_iso}"
color = { key = "free_chapter", default = "FFF9BF" }| Mode | Meaning |
|---|---|
classic |
Normal Discord content/embed/components payload |
multi-message via [[messages]] |
Sends several Discord messages in order |
Many fields support *_when keys:
description = "{chaptername}"
description_when = "chaptername"If chaptername is empty, that field is dropped.
For pure text messages where Discord link previews are unwanted:
suppress_embeds = trueAllowed mentions should be explicit:
[allowed_mentions]
parse = ["roles"]or for messages that should not ping:
[allowed_mentions]
parse = []If the content includes roles but allowed_mentions does not allow roles, the role text may appear without pinging.
Templates support embed fields such as:
[[embeds]]
title = "{title}"
url = "{link}"
description = "{description}"
color = { key = "comments", default = "F0C7A4" }
[embeds.author]
name = "{translator}"
url = "{translator_url}"
url_when = "translator_url"translator_url is only a template placeholder. The scripts fill it from translator_url; do not configure it in config/embeds.json.
[embeds.thumbnail]
url = "{featured_image_url}"
url_when = "featured_image_url"
[embeds.footer]
text = "{host}"
icon_url = "{host_logo_url}"
icon_url_when = "host_logo_url"Templates can include link buttons:
[components]
[[components.action_rows]]
[[components.action_rows.buttons]]
style = "link"
label = "Read here"
url = "{link}"new_arcs.toml uses a multi-message shape:
[[messages]]
name = "header"
content = "..."
[[messages]]
name = "unlocked"
when = "has_unlocked"
content = "..."
[[messages]]
name = "locked"
content = "..."The Python checker builds one context, then render_message_sequence(...) sends the enabled messages in order.
Needs free-feed items with:
title
link
chapter
chaptername
host
short_code
pub_date/guid
Needs paid-feed items with:
title
link
chapter
chaptername
host
short_code
category containing paid/locked/advance info
pub_date/guid
Needs comment-feed items with:
title
link
author
comment_title/comment body/reply chain where available
host
short_code
pub_date/guid
Detects first drops like:
Chapter 1
Ch. 1
Episode 1
Ep. 1
1.1
Prologue
Uses RSS metadata + novel TOML to build a launch announcement.
Uses paid feed + novel history_file.
If a novel has:
history_file = "arc_history/amlwc_history.json"then arc tracking can run.
If it has:
history_file = ""then the checker safely skips arc tracking for that novel.
Detects side stories/extras from chapter labels.
Supports:
- paid completion
- free completion/unlocked full series
- only-free completion
Uses fields such as:
chapter_count = "93 Chapters"
last_chapter = "Chapter 93"
start_date = ""If start_date = "", the duration phrase is safely omitted.
Arc history files live in:
arc_history/
Example:
arc_history/amlwc_history.json
Each file tracks which arcs were already announced so the bot does not repost old arcs.
For a new arc-tracked novel:
- Add
history_filein the novel TOML inrss-feed. - Create the matching JSON file in this repo.
- Initialize it with valid JSON:
{}The current arc checker saves history even when an announcement is skipped, so stale old arcs do not keep triggering.
By default, the arc checker treats the first detected arc as a bootstrap setup step. This prevents old or existing Arc 1 data from being announced accidentally when arc tracking is first added.
The switch lives in config/server.json:
"announce_first_arc_release": false
"announce_first_chapter_release": falseThe first detected arc is saved into arc history, but no first arc announcement is posted.
This works for:
- free-only first arc
- paid-only first arc
- first run where unlocked and locked arc sections both have content
The arc checker only renders sections that have content:
has_unlocked = trueshows the Unlocked sectionhas_locked = trueshows the Locked section
So a free-only first arc will not show an empty Locked embed, and a paid-only first arc will not show an empty Unlocked embed.
First chapter/arc announcements are also delayed until the new novel launch announcement has been recorded in state. This prevents the first arc announcement from posting before the new novel launch message.
NSFW status comes from the RSS/novel metadata, not the Discord mapping.
For RSS-generated items, the category can include NSFW text.
In rss-feed novel TOML:
is_nsfw = trueThe Discord bot can then add the NSFW role from:
config/roles.jsonThe series role and NSFW role are joined safely, so missing pieces do not create duplicate spaces.
State files prevent duplicate posts.
Current files:
state.json
state_rss.json
arc_history/*.json
The RSS state tracks seen GUIDs and last post times.
If a state file becomes empty or invalid, the bot can crash with:
JSONDecodeError
Fix it by committing valid JSON:
{}Runs the free and paid chapter bots.
Triggered by:
repository_dispatch
workflow_dispatch
Runs the comments bot.
Triggered by:
repository_dispatch
workflow_dispatch
Runs checker-style announcements such as:
new arcs
new extras
completion checks
Triggered by:
repository_dispatch
schedule
workflow_dispatch
Free chapter announcements can optionally trigger a status card refresh back in the rss-feed repo.
This flow is intentionally optional and non-fatal. If the callback config is missing, disabled, unreachable, or PAT_GITHUB is not configured, the free chapter announcement should still post normally.
Flow:
rss-feed updates free_chapters_feed.xml
→ rss-feed dispatches discord-webhook with feed=free
→ bot_free_chapters.py posts the new free chapter message
→ status_update_dispatcher.py checks rss-feed config/integrations.json
→ if card_status_update.enabled=true, it sends repository_dispatch to rss-feed
→ rss-feed runs update_novel_status.yml
→ tools/update_novel_status.py updates the configured novel status card embeds
Required file in this repo:
status_update_dispatcher.py
bot_free_chapters.py calls this after free chapter posts are sent:
trigger_status_update(title, host)The dispatcher reads the integration config from the URL configured in:
{
"rss_feed_integrations_url": "https://raw.githubusercontent.com/Cannibal-Turtle/rss-feed/main/config/integrations.json"
}That value belongs in:
config/files.json
Required config in rss-feed/config/integrations.json:
{
"card_status_update": {
"enabled": true,
"repo": "Cannibal-Turtle/rss-feed",
"event_type": "update-novel-status"
}
}Required secret in the discord-webhook repo:
PAT_GITHUB
PAT_GITHUB is only required for the optional status card update callback.
If PAT_GITHUB is missing, status_update_dispatcher.py should skip the callback and print a warning instead of crashing the free chapter bot.
Example skip behavior:
⚠️ PAT_GITHUB missing; skipped optional card status update.
The chapter posting flow must not depend on this callback succeeding.
Manual maintenance workflow for patching/fixing embeds by URL.
Create:
rss-feed/mappings/novels/code.toml
Required basics:
host = "Mistmint Haven"
title = "Novel Title"
short_code = "CODE"
novel_url = "https://..."
featured_image = "https://..."
has_free = true
has_paid = true
has_comments = true
is_nsfw = false
is_membership = falseOptional status/checker fields:
chapter_count = "93 Chapters"
last_chapter = "Chapter 93"
start_date = ""
history_file = ""
discord_color = "#c90016"Edit:
config/novel_discord_map.toml
Add:
[CODE]
role_id = "..."
custom_emoji = "<:...:...>"
role_url = "https://discord.com/channels/..."Edit:
config/tag_roles.json
Only add tags that should ping.
If the novel uses arc tracking:
history_file = "arc_history/code_history.json"Then create:
arc_history/code_history.json
with:
{}Make sure the novel TOML flags match the feeds you expect:
has_free = true
has_paid = true
has_comments = trueFor a new host:
- Add a host TOML file in
rss-feed/mappings/hosts/. - Add novel TOML files in
rss-feed/mappings/novels/. - Implement host utilities in
rss-feed/host_utils/if needed. - Ensure RSS feeds include the host’s items.
- Add Discord role/emoji/role URL data in this repo if announcements should ping.
- Add feed/channel handling here only if the host needs different Discord behavior.
Most new host metadata belongs in rss-feed, not this repo.
Check:
state_rss.json
seen GUID keys
last post time keys
config/feeds.json
Make sure state files are committed after successful workflow runs.
Check:
config/novel_discord_map.tomlhas the correctrole_id.- The template has
allowed_mentionswith roles enabled. - The bot role has permission to mention the role.
- The role is mentionable or the bot has enough permission to ping it.
Check config/embeds.json.
Valid colors:
"FFF9BF"
"#FFF9BF"
"novel"Invalid colors:
"yellow"
"FFF"
"not-a-color"If using "novel", make sure the novel TOML has:
discord_color = "#c90016"or another supported novel color field.
JSON does not allow comments or trailing commas.
Bad:
{
"state_path": "state.json", // comment
}Good:
{
"state_path": "state.json"
}Check:
history_file = "arc_history/code_history.json"If the field is empty, the skip is intentional.
Also confirm the history file exists and contains valid JSON.
Check:
chapter_count = "93 Chapters"
last_chapter = "Chapter 93"If last_chapter does not match the feed item’s chapter label, completion may not trigger.
- Discord-specific role IDs, emojis, and role URLs live in this repo.
- Novel metadata lives in
rss-feed. HOSTING_SITE_DATAremains import-compatible throughnovel_mappings.py.- Split TOML mappings are supported through the installed
rss-feedpackage. - Embed colors can use fixed hex values or
"novel"where supported. "novel"colors resolve to novel color fields from RSS novel TOML.- Empty
history_filesafely skips arc tracking. - Empty
start_datesafely removes the duration phrase from completion messages. - State files prevent duplicate chapter/comment announcements.
- Arc history prevents duplicate arc announcements.
- Cross-repo dispatch can trigger notifier workflows automatically.
rss-feed regenerates XML feeds
↓
rss-feed dispatches event to discord-webhook
↓
discord-webhook installs latest rss-feed package
↓
bot scripts read feeds + HOSTING_SITE_DATA
↓
Discord announcements are posted
↓
state/history files are updated and committed
Before running the notifier:
rss-feedhas the novel TOML file.rss-feedhas the correct feed flags:has_freehas_paidhas_comments
- This repo has the short code in
config/novel_discord_map.toml. config/embeds.jsonhas valid colors.- If using
"novel"colors, the novel TOML has a valid color field. - If using arc tracking,
history_fileis set and the history JSON exists. - Required Discord channel secrets exist.
DISCORD_BOT_TOKENexists.- The workflow installs the latest
rss-feedpackage. - State/history files are committed after successful runs.
Advance Chapters
|
Free Chapters
|
Comments
|
|


