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.
Functions for generating AI-powered daily digests from Discord message exports, formatting them as Obsidian-compatible markdown, and pushing to GitHub.
run_digest_pipeline()
async def run_digest_pipeline(
bot: commands.Bot,
config: Config,
hours: int
) -> Optional[dict]:
"""Run the complete digest pipeline: export, analyze, save."""
from discord.ext import commands
from bot import run_digest_pipeline, Config
bot = commands.Bot(command_prefix="!", intents=intents)
config = Config.from_env()
# Run complete pipeline
result = await run_digest_pipeline(bot, config, 24)
if result["success"]:
print(f"Digest saved to: {result['digest_path']}")
print(f"Stats: {result['stats']}")
print(f"Preview: {result['digest'][:500]}")
else:
print(f"Failed: {result['error']}")
Executes the complete digest generation pipeline: exports messages, generates AI digest with Claude, formats as Obsidian markdown, saves locally, and pushes to GitHub.
Parameters
Discord bot instance with active connection.
Configuration with API keys, paths, and digest settings.
Time range in hours for message export.
Returns
Result dictionary with success status, digest content, and metadata.
Success Response:
{
"success": True,
"digest": "# Individual Updates\n\n**Alice**..." # Full markdown digest
"digest_path": "Daily Digests/2026-03-04 - Team Digest.md",
"stats": {
"total_messages": 1547,
"active_channels": 8,
"contributors": {"Alice", "Bob", "Charlie"},
"contributor_count": 15
},
"export_filename": "exports/discord_export_20260304_000000_ET.json"
}
Error Response:
{
"success": False,
"error": "Export failed" # or "Failed to generate digest"
}
Pipeline Steps
- Export: Calls
perform_export() to gather messages from all channels
- Generate: Calls
generate_daily_digest() to create AI summary with Claude
- Save: Calls
save_digest() to format as Obsidian markdown and save to disk
- Push: Automatically commits and pushes to GitHub if configured
This is the main entry point for digest generation. Use this function instead of calling individual steps.
generate_daily_digest()
async def generate_daily_digest(
export_data: dict,
config: Config
) -> Optional[str]:
"""Use Claude to generate structured daily digest."""
from bot import generate_daily_digest, perform_export, Config
config = Config.from_env()
# First export the data
result = await perform_export(bot, config, 24)
export_data = result["export_data"]
# Generate AI digest
digest = await generate_daily_digest(export_data, config)
if digest:
print(digest) # Markdown formatted digest
else:
print("Failed to generate digest")
Uses Claude API to analyze Discord transcript and generate a structured daily digest with individual updates, blockers, decisions, and action items.
Parameters
Export data dictionary from perform_export() containing channels and messages.
Configuration with anthropic_api_key, digest_model, and digest_max_tokens.
Returns
Markdown-formatted digest text generated by Claude, or None if API call fails.
Example Output:
# Individual Updates
**Alice**
- Completed the authentication refactor, merged PR #234
- Started working on the new dashboard component
- Fixed bug with OAuth token refresh
**Bob**
- Reviewed 5 pull requests
- Updated documentation for the API endpoints
- Deployed v2.1.0 to staging
# Upcoming Work
- Alice: Planning to implement the user profile page this week
- Bob: Will focus on performance optimization next
# Blockers & Challenges
- Charlie is waiting for design mockups before proceeding with the landing page
- Database migration is taking longer than expected
# Key Decisions & Ideas
- Team decided to switch from REST to GraphQL for the new API
- Discussed implementing feature flags for gradual rollout
# Action Items
- [ ] Alice to review Bob's infrastructure PR
- [ ] Schedule meeting about Q2 planning
- [ ] Update staging environment credentials
Digest Structure
Claude is prompted to extract:
- Individual Updates - Work completed or in progress by each team member
- Upcoming Work - Planned tasks and goals mentioned
- Blockers & Challenges - Obstacles and requests for help
- Key Decisions & Ideas - Important discussions and decisions
- Action Items - Specific TODOs and follow-ups
Behavior
- Filters out bot messages using
filter_bot_messages()
- Calls
prepare_transcript() to format messages for Claude
- Uses configured model (default:
claude-haiku-4-5-20251001)
- Returns
None if anthropic_api_key is not configured
- Returns
None if API call fails (logs error)
Requires ANTHROPIC_API_KEY environment variable to be set. Returns None if missing.
prepare_transcript()
def prepare_transcript(export_data: dict) -> str:
"""Prepare transcript for Claude analysis from export data."""
from bot import prepare_transcript
# After export
export_data = result["export_data"]
# Format for Claude
transcript = prepare_transcript(export_data)
print(transcript)
# ## Channel: #general
# [2026-03-04T09:15:30] Alice: Just deployed the new feature
# [2026-03-04T09:16:45] Bob: Looks great! Testing now
# ...
Formats export data into a readable transcript for Claude analysis, organized by channel with timestamps and cleaned message content.
Parameters
Export data dictionary containing channels and messages from perform_export().
Returns
Formatted transcript text with channel sections, timestamps, and messages. Returns “No messages to analyze.” if empty.
Transcript Format:
## Channel: #general
[2026-03-04T09:15:30] Alice: Just deployed the new feature to production
[2026-03-04T09:16:45] Bob: Looks great! Testing the API endpoints now
[2026-03-04T09:18:12] Alice: Let me know if you hit any issues
## Channel: #engineering
[2026-03-04T10:30:00] Charlie: Working on the database migration
[2026-03-04T11:45:20] Alice: Need any help with that?
Behavior
- Groups messages by channel with
## Channel: #name headers
- Filters out bot messages (only includes human messages)
- Uses
content_clean field for readable mentions
- Truncates timestamps to
YYYY-MM-DDTHH:MM:SS format
- Skips channels with no non-bot messages
- Returns fallback text if no messages exist
save_digest()
async def save_digest(
digest_content: str,
date_str: str,
stats: dict,
config: Config
) -> str:
"""Save digest to local directory and push to GitHub."""
from bot import save_digest, Config
from datetime import datetime
config = Config.from_env()
# Save digest
digest_content = "# Individual Updates\n\n**Alice**..."
date_str = datetime.now(config.eastern_tz).strftime("%Y-%m-%d")
stats = {
"total_messages": 1547,
"active_channels": 8,
"contributor_count": 15
}
filepath = await save_digest(digest_content, date_str, stats, config)
print(f"Saved to: {filepath}")
Saves digest content as Obsidian-formatted markdown file with frontmatter, navigation links, and optional GitHub sync.
Parameters
Markdown digest content generated by Claude.
Date string in YYYY-MM-DD format for filename and frontmatter.
Statistics dictionary from calculate_export_stats() with message counts and contributors.
Configuration with directory paths and optional GitHub settings.
Returns
Full file path where digest was saved.
Behavior
- Determines output path using
get_output_path() (Dokploy volume or local)
- Initializes/clones Git repo if
github_repo_url is configured
- Creates directory if it doesn’t exist
- Formats content using
format_obsidian_document()
- Saves to
{date} - Team Digest.md
- Commits and pushes to GitHub if configured
File naming format: 2026-03-04 - Team Digest.md
def format_obsidian_document(
date_str: str,
digest_content: str,
stats: dict,
config: Config
) -> str:
"""Create Obsidian-formatted document."""
from bot import format_obsidian_document, Config
config = Config.from_env()
markdown = format_obsidian_document(
date_str="2026-03-04",
digest_content="# Individual Updates\n...",
stats={"total_messages": 1547, "active_channels": 8, "contributor_count": 15},
config=config
)
print(markdown)
Formats digest content into Obsidian-compatible markdown with YAML frontmatter, navigation links, and metadata.
Parameters
Date in YYYY-MM-DD format.
Main digest content from Claude.
Statistics with total_messages, active_channels, and contributor_count.
Configuration for timezone display.
Returns
Complete markdown document with frontmatter, content, and navigation.
Output Structure:
---
date: 2026-03-04
type: daily-digest
tags: [team, standup, daily]
contributors: 15
messages: 1547
channels: 8
---
# Team Digest - 2026-03-04
**📊 Activity Summary**
- 1547 messages across 8 channels
- 15 active team members
- Time range: Last 24 hours
---
{digest_content}
---
**🔗 Links**
- [[2026-03-03 - Team Digest|← Previous Day]]
- [[2026-03-05 - Team Digest|Next Day →]]
---
*Auto-generated at 12:00 AM ET from Discord*
Features
- YAML Frontmatter: Obsidian-compatible metadata for filtering and search
- Activity Summary: Quick statistics at the top
- Navigation Links: Wikilink-style links to previous/next day digests
- Timestamp: Generation time in Eastern Time
- Tags: Pre-configured tags for categorization in Obsidian
Git Integration Functions
init_git_repo()
def init_git_repo(output_path: str, config: Config) -> bool:
"""Initialize or clone git repository."""
from bot import init_git_repo, Config
config = Config.from_env()
output_path = "/path/to/digests"
success = init_git_repo(output_path, config)
if success:
print("Git repo ready")
else:
print("Git initialization failed")
Initializes Git repository in the output directory or clones from GitHub if not present.
Parameters:
output_path (str): Directory path for digests
config (Config): Configuration with github_repo_url and github_token
Returns: bool - True if successful, False otherwise
Behavior:
- If
.git exists: runs git fetch and git merge origin/main
- If no
.git: clones repository using authenticated URL
- Falls back to
git init + remote add if clone fails
- Requires both
github_repo_url and github_token in config
git_commit_and_push()
def git_commit_and_push(
file_path: str,
date_str: str,
config: Config
) -> bool:
"""Commit and push the digest file to GitHub."""
from bot import git_commit_and_push, Config
config = Config.from_env()
success = git_commit_and_push(
file_path="Daily Digests/2026-03-04 - Team Digest.md",
date_str="2026-03-04",
config=config
)
if success:
print("Pushed to GitHub")
Commits the digest file and pushes to GitHub with an automated commit message.
Parameters:
file_path (str): Full path to digest file
date_str (str): Date string for commit message
config (Config): Configuration with GitHub credentials
Returns: bool - True if successful, False otherwise
Behavior:
- Configures git user as “Discord Bot
<bot@discord.local>”
- Stages the specific digest file
- Creates commit with message:
"Daily digest for {date_str}"
- Pushes to
main branch using authenticated URL
- Returns
False if GitHub not configured
Git operations are synchronous and may block. Consider running in executor for async contexts.
Complete Pipeline Example
from discord.ext import commands, tasks
from bot import Config, run_digest_pipeline
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
config = Config.from_env()
bot = commands.Bot(command_prefix="!", intents=intents)
# Manual trigger via command
@bot.command(name="digest")
async def manual_digest(ctx, hours: int = 24):
"""Generate digest on demand"""
await ctx.send(f"Generating digest for last {hours} hours...")
result = await run_digest_pipeline(bot, config, hours)
if result["success"]:
preview = result["digest"][:500] + "..."
await ctx.send(
f"✅ Digest generated!\n\n{preview}\n\n"
f"Full digest: `{result['digest_path']}`"
)
else:
await ctx.send(f"❌ Error: {result['error']}")
# Automatic scheduled digest
@tasks.loop(hours=24)
async def daily_digest_task():
"""Runs automatically at scheduled time"""
logger.info("Starting scheduled digest generation")
result = await run_digest_pipeline(bot, config, 24)
if result["success"]:
logger.info(f"Digest completed: {result['digest_path']}")
else:
logger.error(f"Digest failed: {result['error']}")
@daily_digest_task.before_loop
async def wait_for_scheduled_time():
"""Wait until midnight ET"""
await bot.wait_until_ready()
now = datetime.now(config.eastern_tz)
target = now.replace(hour=0, minute=0, second=0, microsecond=0)
if now >= target:
target += timedelta(days=1)
wait_seconds = (target - now).total_seconds()
logger.info(f"Waiting {wait_seconds/3600:.1f} hours until first digest")
await asyncio.sleep(wait_seconds)
# Start the scheduled task
@bot.event
async def on_ready():
if not daily_digest_task.is_running():
daily_digest_task.start()