Documentation Index
Fetch the complete documentation index at: https://mintlify.com/trustlessmatt/discord-exporter-bot/llms.txt
Use this file to discover all available pages before exploring further.
Utility functions for timezone conversion, Discord mention handling, message filtering, and API client creation.
convert_to_eastern()
def convert_to_eastern(utc_time: datetime, tz: ZoneInfo) -> datetime:
"""Convert UTC datetime to target timezone (handles DST automatically)."""
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from bot import convert_to_eastern
# UTC time
utc_now = datetime.now(timezone.utc)
# Convert to Eastern Time
eastern_tz = ZoneInfo("America/New_York")
eastern_time = convert_to_eastern(utc_now, eastern_tz)
print(f"UTC: {utc_now}")
print(f"Eastern: {eastern_time}")
# Automatically handles DST transitions
Converts UTC datetime to any timezone with automatic DST (Daylight Saving Time) handling.
Parameters
UTC datetime to convert. If timezone-naive, UTC is assumed.
Target timezone (e.g., ZoneInfo("America/New_York")).
Returns
Datetime converted to target timezone with proper DST handling.
Behavior
- Automatically adds UTC timezone if input is naive
- Converts to target timezone using
astimezone()
- Handles DST transitions automatically via
ZoneInfo
- Works with any valid IANA timezone identifier
DST Example:
from datetime import datetime
from zoneinfo import ZoneInfo
# During DST (EDT = UTC-4)
summer = datetime(2026, 7, 1, 12, 0, 0, tzinfo=timezone.utc)
eastern_summer = convert_to_eastern(summer, ZoneInfo("America/New_York"))
print(eastern_summer) # 2026-07-01 08:00:00-04:00 (EDT)
# During Standard Time (EST = UTC-5)
winter = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
eastern_winter = convert_to_eastern(winter, ZoneInfo("America/New_York"))
print(eastern_winter) # 2026-01-01 07:00:00-05:00 (EST)
Uses Python’s zoneinfo module which reads timezone data from the operating system’s tz database.
clean_content()
def clean_content(content: str, guild) -> str:
"""Replace all Discord mentions with readable text."""
import discord
from bot import clean_content
message_content = "Hey <@123456789> check out <#987654321> and ping <@&555555555>"
# Clean all mentions
cleaned = clean_content(message_content, guild)
print(cleaned)
# Output: "Hey @username check out #general and ping @moderators"
Replaces all Discord mention formats (users, channels, roles) with human-readable names.
Parameters
Raw Discord message content with mention tags.
Guild object for resolving mention IDs to names.
Returns
Content with all mentions replaced by readable names (e.g., @username, #channel, @role).
| Discord Format | Cleaned Format | Example |
|---|
<@123456> | @username | User mention |
<@!123456> | @username | User mention (alternative) |
<#789012> | #channel-name | Channel mention |
<@&456789> | @role-name | Role mention |
Behavior
- Handles both
<@ID> and <@!ID> user mention formats
- Resolves IDs using guild methods:
get_member(), get_channel(), get_role()
- Preserves original mention if entity not found (deleted user/channel/role)
- Returns original content if
content is None or empty
Complex Example:
content = """
Hey <@123> and <@!456>, please review PR in <#789>.
<@&999> team should be notified.
"""
cleaned = clean_content(content, guild)
# Output:
# """
# Hey @alice and @bob, please review PR in #code-review.
# @engineering team should be notified.
# """
If a user, channel, or role has been deleted, the original mention format is preserved in the cleaned output.
replace_mention()
def replace_mention(
pattern: re.Pattern,
content: str,
guild,
resolver
) -> str:
"""Generic function to replace Discord mentions with readable text."""
import re
from bot import replace_mention, USER_MENTION_PATTERN
# Replace only user mentions
content = "Message from <@123456> to <@789012>"
cleaned = replace_mention(
USER_MENTION_PATTERN,
content,
guild,
guild.get_member
)
print(cleaned) # "Message from @alice to @bob"
Generic helper function for replacing Discord mentions using regex patterns and resolver functions.
Parameters
Compiled regex pattern for matching mentions. Use USER_MENTION_PATTERN, CHANNEL_MENTION_PATTERN, or ROLE_MENTION_PATTERN.
Message content to process.
Guild context for resolving mentions.
resolver
Callable[[int], Optional[Entity]]
required
Function to resolve entity ID to entity object (e.g., guild.get_member, guild.get_channel, guild.get_role).
Returns
Content with matched mentions replaced by readable names.
Available Patterns
# Imported from bot.py
USER_MENTION_PATTERN = re.compile(r"<@!?(\d+)>")
CHANNEL_MENTION_PATTERN = re.compile(r"<#(\d+)>")
ROLE_MENTION_PATTERN = re.compile(r"<@&(\d+)>")
Custom Usage Example
import re
from bot import replace_mention
# Only replace channel mentions
channel_content = "Check <#123> and <#456>"
channel_cleaned = replace_mention(
re.compile(r"<#(\d+)>"),
channel_content,
guild,
guild.get_channel
)
# Only replace role mentions
role_content = "Attention <@&999> and <@&888>"
role_cleaned = replace_mention(
re.compile(r"<@&(\d+)>"),
role_content,
guild,
guild.get_role
)
Use clean_content() to replace all mention types at once. Use replace_mention() when you need fine-grained control over which mentions to replace.
filter_bot_messages()
def filter_bot_messages(messages: list) -> list:
"""Filter out messages from bots."""
from bot import filter_bot_messages
# All messages (including bots)
all_messages = [
{"author": {"name": "alice", "bot": False}, "content": "Hello"},
{"author": {"name": "BotUser", "bot": True}, "content": "Automated message"},
{"author": {"name": "bob", "bot": False}, "content": "Hi there"}
]
# Filter to only human messages
human_messages = filter_bot_messages(all_messages)
print(len(human_messages)) # 2
print([m["author"]["name"] for m in human_messages]) # ["alice", "bob"]
Filters out messages from bot accounts, returning only messages from human users.
Parameters
List of serialized message dictionaries (from serialize_message()).
Returns
Filtered list containing only messages where author.bot is False.
Behavior
- Checks
message["author"]["bot"] field
- Returns new list (does not modify input)
- Preserves message order
- Works with serialized message format from
serialize_message()
Usage in Pipeline:
from bot import export_channel_messages, filter_bot_messages, Config
config = Config.from_env()
# Export all messages
all_messages = await export_channel_messages(channel, 24, config)
print(f"Total messages: {len(all_messages)}")
# Filter to humans only
human_messages = filter_bot_messages(all_messages)
print(f"Human messages: {len(human_messages)}")
# Calculate human-only statistics
human_authors = {msg["author"]["display_name"] for msg in human_messages}
print(f"Active humans: {len(human_authors)}")
This function is used internally by prepare_transcript() to exclude bot messages from digest generation.
get_anthropic_client()
def get_anthropic_client(api_key: str) -> anthropic.Anthropic:
"""Get or create Anthropic client (reusable)."""
import anthropic
from bot import get_anthropic_client, Config
config = Config.from_env()
# Create client
client = get_anthropic_client(config.anthropic_api_key)
# Use client for API calls
message = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
print(message.content[0].text)
Creates an Anthropic API client instance for calling Claude.
Parameters
Anthropic API key (starts with sk-ant-api03-).
Returns
Initialized Anthropic client ready for API calls.
Behavior
- Creates new
anthropic.Anthropic instance
- Client handles rate limiting and retries automatically
- Can be reused for multiple API calls
- Thread-safe for concurrent requests
Advanced Example:
from bot import get_anthropic_client, Config
import asyncio
config = Config.from_env()
client = get_anthropic_client(config.anthropic_api_key)
def analyze_text(text: str) -> str:
"""Analyze text with Claude"""
response = client.messages.create(
model=config.digest_model,
max_tokens=config.digest_max_tokens,
messages=[{
"role": "user",
"content": f"Summarize this:\n\n{text}"
}]
)
return response.content[0].text
# Use in async context
async def analyze_multiple(texts: list[str]):
"""Analyze multiple texts concurrently"""
loop = asyncio.get_event_loop()
tasks = [loop.run_in_executor(None, analyze_text, text) for text in texts]
return await asyncio.gather(*tasks)
Store API keys securely in environment variables. Never hardcode API keys in source code.
get_output_path()
def get_output_path(config: Config) -> str:
"""Determine the output path for digests."""
from bot import get_output_path, Config
import os
config = Config.from_env()
# Get appropriate output path
output_path = get_output_path(config)
print(f"Saving digests to: {output_path}")
# Use for file operations
digest_file = f"{output_path}/2026-03-04 - Team Digest.md"
os.makedirs(output_path, exist_ok=True)
Determines the appropriate output directory for digest files, preferring Dokploy volume if available.
Parameters
Configuration with dokploy_volume_path and digests_dir settings.
Returns
Full path to directory where digests should be saved.
Path Resolution Logic
-
If Dokploy volume configured and exists:
- Returns:
{dokploy_volume_path}/{digests_dir}
- Example:
/var/lib/dokploy/volumes/bot/Daily Digests
- Logs: “Saving to dokploy volume: …”
-
Otherwise:
- Returns:
{digests_dir} (local directory)
- Example:
Daily Digests
- Logs: “Dokploy volume not found, saving to local: …”
Behavior
- Checks if
dokploy_volume_path is set in config
- Verifies path exists using
os.path.exists()
- Falls back to local directory if Dokploy path unavailable
- Logs decision for debugging
- Does not create directories (caller’s responsibility)
Docker/Dokploy Example:
# In Dokploy deployment with volume mounted at /data
config.dokploy_volume_path = "/data"
config.digests_dir = "Daily Digests"
path = get_output_path(config)
# Returns: "/data/Daily Digests" (persistent across container restarts)
# In local development without Dokploy
config.dokploy_volume_path = None # or path doesn't exist
config.digests_dir = "Daily Digests"
path = get_output_path(config)
# Returns: "Daily Digests" (local directory)
In production Docker deployments, mount a volume to the Dokploy path to persist digests across container restarts.
Complete Utility Example
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from bot import (
Config,
convert_to_eastern,
clean_content,
filter_bot_messages,
get_anthropic_client,
get_output_path
)
import discord
# Initialize
config = Config.from_env()
guild = bot.get_guild(config.guild_id)
# Timezone conversion
utc_time = datetime.now(timezone.utc)
eastern_time = convert_to_eastern(utc_time, config.eastern_tz)
print(f"Current time in ET: {eastern_time.strftime('%I:%M %p')}")
# Content cleaning
raw_message = "Hey <@123456> check <#789012>"
clean_message = clean_content(raw_message, guild)
print(f"Clean message: {clean_message}")
# Message filtering
all_messages = [
{"author": {"bot": False}, "content": "Hello"},
{"author": {"bot": True}, "content": "[Bot] Automated"},
{"author": {"bot": False}, "content": "Thanks!"}
]
human_only = filter_bot_messages(all_messages)
print(f"Human messages: {len(human_only)}/
# API client
client = get_anthropic_client(config.anthropic_api_key)
response = client.messages.create(
model=config.digest_model,
max_tokens=100,
messages=[{"role": "user", "content": "Hello Claude!"}]
)
print(f"Claude says: {response.content[0].text}")
# Output path
output = get_output_path(config)
print(f"Digests will be saved to: {output}")