with one click
discord-bot-operations
Manage Discord bot access, permissions, multi-channel publishing, and aggregated content delivery for automated reporting workflows
Menu
Manage Discord bot access, permissions, multi-channel publishing, and aggregated content delivery for automated reporting workflows
| name | discord-bot-operations |
| category | social-media |
| description | Manage Discord bot access, permissions, multi-channel publishing, and aggregated content delivery for automated reporting workflows |
| triggers | ["discord bot access check","discord channel permissions","403 errors from Discord API","publishing large digests to Discord","multi-source content aggregation for Discord"] |
Covers the full lifecycle of getting content from multiple scrapers into Discord channels: verifying bot membership/permissions, diagnosing 403 errors, aggregating outputs from different data sources, handling Discord’s message size limits, and publishing to the correct channel IDs.
403: bot lacks access to channel or cannot deliver to a Discord targetBefore any bot operations, ensure:
bot scopeView Channel and Send Messages permissions in each target channelDo this whenever a Discord target fails or after a new bot invite:
Tip: If the user says they have administrator privileges but the bot still fails, the bot may simply not be in the guild yet. Generate an OAuth2 invite (scopes:
bot; permissions:Send Messages,View Channel) and add it, then re-test.
If delivery fails with 403:
deliver value.Additional checks:
DISCORD_BOT_TOKEN is set in the environment (typically $HERMES_HOME/.env). A missing token will cause an unauthenticated request, resulting in 403 Forbidden. Check with: echo $DISCORD_BOT_TOKEN or grep DISCORD_BOT_TOKEN $HERMES_HOME/.env.curl -H "Authorization: Bot <TOKEN>" https://discord.com/api/v10/users/@me. A 401/403 response indicates an invalid token.When a single digest must cover multiple domains (e.g., movies + anime + TV):
daily_cinema_chatter.py → saves ~/movie_digest_today.mdmal_seasonal_scraper.py → saves to ~/obsidian-vault/FACorreia/Raw/Anime/Seasonal/<season> Seasonal Anime & Movies.mdtvshows_seasonal_scraper.py → saves to ~/obsidian-vault/FACorreia/Raw/TV/Seasonal/<season> TV Shows.mdnews_digest.py or daily_stock_news.py as neededChoose the delivery format based on content size and user preference:
Option A — Single File Attachment (simple, for very large content)
send_message..md or .txt file using MEDIA:/path/to/file.Option B — Multi-Post Sequential Delivery (PREFERRED for large digests) User preference: inline formatting with full digest attached only on final post. Matches Slack delivery style.
MEDIA:/path/to/full_digest.mdExample sequence for a 9-part digest:
- Message 1: Intro "📈 Daily Stock Digest — Part 1 of 9..."
- Messages 2–9: Batch content
- Message 10 (final): "Part 9 of 9 — summary... [attached full MD]"
Why this format?
news_digest.py outputs (~2178 chars).When the same digest is delivered to multiple platforms (Discord + Slack + Telegram):
daily-stock-news vs daily-stock-news-triple). Consolidate to avoid spam.1 to mean "alert found" rather than "transport failed". Treat that as success only when the script produced valid alert output and the wrapper contract says so.HERMES_HOME than the authoring shell. Load env from multiple Hermes .env locations and support export KEY=... lines.Symptoms
urllib.request or requests librariescurl or when a User-Agent header is presentRoot Cause
Discord's API may reject requests from Python's standard library urllib or requests due to missing or non-browser-like User-Agent strings. These libraries present a generic User-Agent that Discord's WAF may flag as non-standard traffic, resulting in 403 Forbidden responses. This is similar to, but distinct from, TLS fingerprinting issues.
Investigation Steps
Test with curl to confirm the token and channel are valid:
curl -X POST https://discord.com/api/v10/channels/<CHANNEL_ID>/messages \
-H "Authorization: Bot <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"content":"test"}'
If curl succeeds but Python fails, the issue is likely the User-Agent.
Check the Python script for missing User-Agent headers.
Recommended Solutions
Option 1: Add a User-Agent header (Recommended)
Add a User-Agent header to your request to mimic a browser or bot client:
headers={
'Authorization': f'Bot {token}',
'Content-Type': 'application/json',
'User-Agent': 'HermesBot/1.0' # or 'Mozilla/5.0' for generic browser
}
This simple fix often resolves 403 errors immediately.
Option 2: Use a library that mimics curl's fingerprint
Libraries like curl_cffi or undetected-chromedriver can mimic browser TLS fingerprints, but they add complexity.
Prevention
urllib or requests.HermesBot/1.0).Symptoms
urllib or requests librariescurlRoot Cause
Discord uses Cloudflare as a WAF which employs TLS fingerprinting to detect and block non-browser traffic. Python's standard library urllib and requests have distinctive TLS fingerprints that Cloudflare recognizes and challenges, resulting in 403 Forbidden responses. curl presents a more browser-like fingerprint and passes Cloudflare's checks.
Investigation Steps
Test with curl to confirm the issue:
curl -X POST https://discord.com/api/v10/channels/<CHANNEL_ID>/messages \
-H "Authorization: Bot <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"content":"Test"}'
If curl succeeds but Python fails, Cloudflare blocking is the cause.
Check Python script for use of urllib.request or requests. Look for patterns like:
import urllib.request
# or
import requests
Verify token and channel are correct by making a direct API call with the same Python code to a non-Cloudflare endpoint (e.g., https://discord.com/api/v10/users/@me). If this also returns 403, the token may be invalid. If it works, the issue is specific to the Discord messages endpoint.
Recommended Solutions
Option 1: Switch to curl via subprocess (Recommended)
Replace urllib or requests with a curl call in the script:
import subprocess
import json
try:
payload = json.dumps({'content': output[:2000]})
cmd = [
'curl', '-X', 'POST',
f'https://discord.com/api/v10/channels/{channel}/messages',
'-H', f'Authorization: Bot {token}',
'-H', 'Content-Type: application/json',
'--data', payload
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if result.returncode != 0 or 'message' in result.stdout:
print(f"⚠️ Discord send failed: {result.stderr}", file=sys.stderr)
else:
print("✓ Alert sent to Discord", file=sys.stderr)
except Exception as e:
print(f"⚠️ Discord send failed: {e}", file=sys.stderr)
Option 2: Use a library that mimics curl's fingerprint
Libraries like curl_cffi or undetected-chromedriver can mimic browser TLS fingerprints, but they add complexity.
Option 3: Whitelist the server IP in Cloudflare
Requires Discord server admin access and is generally not recommended due to security implications.
Symptoms
HTTP Error 403: Forbidden on all endpoints.env but is longer than expected (e.g., 72 characters instead of 59)curl and Python HTTP libraries fail with 403Root Cause Discord bot tokens have a fixed length of 59 characters. A token that is significantly longer indicates:
Investigation Steps
grep DISCORD_BOT_TOKEN /opt/data/.env | cut -d'=' -f2 | tr -d '\"' | wc -c
# Expected: 59
# Actual: 72 (or other length)
Mj, Bx, MS, or MWcurl -H "Authorization: Bot <TOKEN>" https://discord.com/api/v10/users/@me
# Expected: 200 OK with user info
# Actual: 403 Forbidden (invalid token)
Resolution
sed -i 's/DISCORD_BOT_TOKEN=.*/DISCORD_BOT_TOKEN=new_59_char_token/' /opt/data/.env
grep DISCORD_BOT_TOKEN /opt/data/.env | cut -d'=' -f2 | tr -d '\"' | wc -c
# Should output: 59
Prevention
.env filesscripts/verify_discord_access.py)Symptoms
HTTP Error 403: Forbidden when sending messagescurl works, but Python script failsRoot Cause Inconsistent channel ID usage across scripts and configuration files can cause:
Investigation Steps
grep -r "channel.*id\|channel_id" /opt/data/home/.hermes/scripts/ --include="*.py"
grep DISCORD_.*CHANNEL /opt/data/.env
cat /opt/data/channel_directory.json | jq '.channels[] | {id: .id, name: .name}'
Resolution
channel_id = os.environ.get('DISCORD_STOCK_CHANNEL_ID', 'default_id')
.env match the intended targetsPrevention
.env with commentsreferences/discord-permission-troubleshooting.md — detailed 403 diagnosis, OAuth2 scopes, role hierarchy, override resolutionreferences/content-scripts-inventory.md — catalog of available scrapers with paths, outputs, and schedulesreferences/channel-id-mapping.md — user's Discord channel IDs and their purposesreferences/facorreia-channel-mapping.md — actual channel IDs discovered from live channel_directory.json for the FACorreia guild; up-to-date mapping of channel names → snowflake IDsscripts/verify_discord_access.py — one-shot script to test bot access across all configured channelsscripts/batch_digest_for_discord.py — batching utility: splits long markdown into Discord-safe sequential parts (≤1800 chars each), preserves section boundaries; ideal for multi-post digest delivery with final attachmentreferences/self-delivering-discord-403.md — session notes on self-delivering alert scripts, direct Discord 403 diagnosis, token/channel validation, and deliver=origin/local routing.DISCORD_BOT_TOKEN is typically set in $HERMES_HOME/.env (e.g., /opt/data/.env) and read by the gateway at runtime. The token is NOT stored in ~/.hermes/config.yaml.$HERMES_HOME/channel_directory.json (e.g., /opt/data/channel_directory.json). This file is created/refreshed by the gateway on startup and every 5 minutes. It will not exist if the gateway is not running.~/.hermes/config.yaml, but platform credentials are read from $HERMES_HOME/.env and $HERMES_HOME/config.yaml as fallback. HERMES_HOME defaults to /opt/data in Docker installs.These were found in channel_directory.json during a live run:
1498025894751768776 → #hermes (home channel for general digests)1499338003334561843 → #stock-news1499331939469889656 → #tv-anime-and-movies1498815493757341896 → #stock-alerts1499908671847661578 → #swift1499908914500862123 → #golang
(Use these as defaults when targeting the user's server.)Scan vault raw/ folder for hyperlinks embedded in markdown notes; POST each unseen URL to LuminaVault server /v1/capture/safari so Hermes ingests + memorizes. Usage: /kb-vacuum [folder=raw/]
Compile all uncompiled raw/ content into the wiki. Writes source summaries, creates/updates concept articles with Obsidian backlinks, and updates the index. Run after /kb-ingest to process new content.
Generate daily Reddit and X/Twitter marketing content from the AI Cohort scoreboard, with automatic fallback to the most recent available data when today's file is missing.
Configuration, maintenance, and troubleshooting of AI Cohort scoreboard scripts including vault path setup and script updates.
Build and deliver periodic content digests (news, stock, entertainment) to multiple platforms: save to vault Raw/ and print full markdown to stdout for cron-based delivery.
Capture external content (X/Twitter articles, web posts) and ingest into Obsidian vault Raw/ with automatic theme detection, summarization, and structured frontmatter. Handles X/fixupx links via multi-strategy extraction (direct fetch → r.jina.ai → nitter fallback).