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.
Overview
The message export system is the foundation of the Discord Exporter Bot. It concurrently fetches messages from all text channels in a Discord server, serializes them into a structured JSON format, and calculates comprehensive statistics about server activity.
How Message Export Works
The export process happens in three main stages:
- Concurrent Channel Export - All channels are processed in parallel using
asyncio.gather
- Message Serialization - Each message is converted to a rich JSON object
- Statistics Calculation - Aggregate data is computed from the export
Core Export Function
The export_channel_messages function handles fetching messages from a single channel:
async def export_channel_messages(channel, hours: int, config: Config) -> list:
"""Export messages from a channel within the last X hours."""
cutoff_time = datetime.now(timezone.utc) - timedelta(hours=hours)
messages_data = []
guild = channel.guild
try:
async for message in channel.history(limit=None, after=cutoff_time):
messages_data.append(serialize_message(message, guild, config))
logger.info(f"Exported {len(messages_data)} messages from #{channel.name}")
return messages_data
except discord.Forbidden:
logger.warning(f"No permission to read #{channel.name}")
return []
except Exception as e:
logger.error(f"Error exporting #{channel.name}: {e}")
return []
Key features:
- Time-based filtering using
cutoff_time parameter
- Graceful handling of permission errors
- Returns empty list on failure (fail-safe design)
- Logs export progress for monitoring
Concurrent Channel Exports
The bot uses asyncio.gather to export all channels simultaneously, dramatically improving performance:
# From perform_export function in bot.py:184-257
# Export all channels concurrently for better performance
tasks_list = [export_channel_messages(ch, hours, config) for ch in guild.text_channels]
results = await asyncio.gather(*tasks_list)
for channel, messages in zip(guild.text_channels, results):
if messages:
export_data["channels"][channel.name] = {
"channel_id": str(channel.id),
"messages": messages
}
Performance benefits:
- A server with 10 channels exports in ~10-15 seconds instead of 100+ seconds
- Network I/O happens in parallel rather than sequentially
- Results maintain channel order using
zip
Message Serialization
The serialize_message function converts Discord message objects into a comprehensive JSON structure:
def serialize_message(message, guild, config: Config) -> dict:
"""Convert a Discord message to a serializable dictionary."""
eastern_time = convert_to_eastern(message.created_at, config.eastern_tz)
edited_eastern = convert_to_eastern(message.edited_at, config.eastern_tz) if message.edited_at else None
return {
"message_id": str(message.id),
"author": {
"name": message.author.name,
"display_name": message.author.display_name,
"id": str(message.author.id),
"bot": message.author.bot
},
"content": message.content,
"content_clean": clean_content(message.content, guild),
"timestamp": eastern_time.isoformat(),
"timestamp_utc": message.created_at.isoformat(),
"edited_timestamp": edited_eastern.isoformat() if edited_eastern else None,
"attachments": [
{"filename": att.filename, "url": att.url, "size": att.size}
for att in message.attachments
],
"embeds": len(message.embeds),
"reactions": [
{"emoji": str(reaction.emoji), "count": reaction.count}
for reaction in message.reactions
],
"mentions": [
{"id": str(user.id), "name": user.name, "display_name": user.display_name}
for user in message.mentions
],
"channel_mentions": [
{"id": str(ch.id), "name": ch.name}
for ch in message.channel_mentions
],
"thread": message.thread.name if hasattr(message, "thread") and message.thread else None
}
What gets captured:
- Author information (username, display name, bot status)
- Both raw and cleaned content (mentions resolved)
- Dual timestamps (Eastern Time + UTC)
- Complete attachment metadata
- Reactions with counts
- User and channel mentions
- Thread information
Mention Cleaning
Discord uses special syntax for mentions (<@123>, <#456>, <@&789>). The bot converts these to readable text:
def replace_mention(pattern: re.Pattern, content: str, guild, resolver) -> str:
"""Generic function to replace Discord mentions with readable text."""
if not content:
return content
def replace(match):
entity_id = int(match.group(1))
entity = resolver(entity_id)
if entity:
prefix = "@" if pattern in (USER_MENTION_PATTERN, ROLE_MENTION_PATTERN) else "#"
return f"{prefix}{entity.name}"
return match.group(0)
return pattern.sub(replace, content)
def clean_content(content: str, guild) -> str:
"""Replace all Discord mentions with readable text."""
content = replace_mention(USER_MENTION_PATTERN, content, guild, guild.get_member)
content = replace_mention(CHANNEL_MENTION_PATTERN, content, guild, guild.get_channel)
content = replace_mention(ROLE_MENTION_PATTERN, content, guild, guild.get_role)
return content
Regex patterns (defined in bot.py:77-79):
USER_MENTION_PATTERN = re.compile(r"<@!?(\d+)>")
CHANNEL_MENTION_PATTERN = re.compile(r"<#(\d+)>")
ROLE_MENTION_PATTERN = re.compile(r"<@&(\d+)>")
Example transformation:
<@123456> β @JohnDoe
<#789012> β #general
<@&345678> β @Developers
JSON Output Structure
The final export is saved with comprehensive metadata:
{
"guild_name": "My Discord Server",
"export_time_eastern": "2026-03-04T19:30:00-05:00",
"export_time_utc": "2026-03-05T00:30:00+00:00",
"timezone": "America/New_York (Eastern Time - auto DST)",
"time_range_hours": 24,
"channels": {
"general": {
"channel_id": "123456789",
"messages": [
{
"message_id": "987654321",
"author": {
"name": "john_doe",
"display_name": "John Doe",
"id": "111222333",
"bot": false
},
"content": "Hey <@444555666>, check out <#789012>!",
"content_clean": "Hey @jane_smith, check out #announcements!",
"timestamp": "2026-03-04T14:23:15-05:00",
"timestamp_utc": "2026-03-04T19:23:15+00:00",
"edited_timestamp": null,
"attachments": [
{
"filename": "screenshot.png",
"url": "https://cdn.discordapp.com/attachments/...",
"size": 245678
}
],
"embeds": 0,
"reactions": [
{"emoji": "π", "count": 5},
{"emoji": "π", "count": 2}
],
"mentions": [
{
"id": "444555666",
"name": "jane_smith",
"display_name": "Jane Smith"
}
],
"channel_mentions": [
{"id": "789012", "name": "announcements"}
],
"thread": null
}
]
}
}
}
File naming convention:
filename = f"{config.exports_dir}/discord_export_{export_time_eastern.strftime('%Y%m%d_%H%M%S')}_ET.json"
# Example: exports/discord_export_20260304_193000_ET.json
Statistics Calculation
The bot computes aggregate statistics from the export data:
def calculate_export_stats(export_data: dict) -> dict:
"""Calculate statistics from export data."""
total_messages = sum(len(ch["messages"]) for ch in export_data["channels"].values())
active_channels = len(export_data["channels"])
contributors = {
msg["author"]["display_name"]
for channel_data in export_data["channels"].values()
for msg in channel_data["messages"]
if not msg["author"]["bot"]
}
return {
"total_messages": total_messages,
"active_channels": active_channels,
"contributors": contributors,
"contributor_count": len(contributors)
}
Statistics provided:
total_messages - Sum of all messages across channels
active_channels - Count of channels with messages
contributors - Set of unique non-bot display names
contributor_count - Number of unique contributors
Bot filtering:
if not msg["author"]["bot"] # Excludes bot messages from contributor count
Usage Example
# In Discord
!export 24 # Export last 24 hours
!export 168 # Export last week (168 hours)
Bot response:
β
Export complete!
β’ 1,247 messages
β’ 8 channels
β’ Saved to `exports/discord_export_20260304_193000_ET.json`
- Concurrent execution: All channels export simultaneously
- Memory efficient: Streams messages rather than loading all into memory
- Error resilient: Individual channel failures donβt stop the entire export
- Rate limit aware: Discord.py handles rate limiting automatically
Typical export times:
- Small server (5 channels, 500 messages): ~3-5 seconds
- Medium server (15 channels, 2000 messages): ~8-12 seconds
- Large server (30 channels, 5000 messages): ~15-20 seconds