Skip to main content

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)."""
Converts UTC datetime to any timezone with automatic DST (Daylight Saving Time) handling.

Parameters

utc_time
datetime
required
UTC datetime to convert. If timezone-naive, UTC is assumed.
tz
ZoneInfo
required
Target timezone (e.g., ZoneInfo("America/New_York")).

Returns

return
datetime
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."""
Replaces all Discord mention formats (users, channels, roles) with human-readable names.

Parameters

content
str
required
Raw Discord message content with mention tags.
guild
discord.Guild
required
Guild object for resolving mention IDs to names.

Returns

return
str
Content with all mentions replaced by readable names (e.g., @username, #channel, @role).

Mention Formats

Discord FormatCleaned FormatExample
<@123456>@usernameUser mention
<@!123456>@usernameUser mention (alternative)
<#789012>#channel-nameChannel mention
<@&456789>@role-nameRole 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."""
Generic helper function for replacing Discord mentions using regex patterns and resolver functions.

Parameters

pattern
re.Pattern
required
Compiled regex pattern for matching mentions. Use USER_MENTION_PATTERN, CHANNEL_MENTION_PATTERN, or ROLE_MENTION_PATTERN.
content
str
required
Message content to process.
guild
discord.Guild
required
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

return
str
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."""
Filters out messages from bot accounts, returning only messages from human users.

Parameters

messages
list[dict]
required
List of serialized message dictionaries (from serialize_message()).

Returns

return
list[dict]
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)."""
Creates an Anthropic API client instance for calling Claude.

Parameters

api_key
str
required
Anthropic API key (starts with sk-ant-api03-).

Returns

return
anthropic.Anthropic
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."""
Determines the appropriate output directory for digest files, preferring Dokploy volume if available.

Parameters

config
Config
required
Configuration with dokploy_volume_path and digests_dir settings.

Returns

return
str
Full path to directory where digests should be saved.

Path Resolution Logic

  1. 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: …”
  2. 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}")

Build docs developers (and LLMs) love