Initial commit: Discord TLDR - automated server summaries
This commit is contained in:
commit
2378565a79
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
88
README.md
Normal file
88
README.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# Discord TLDR
|
||||||
|
|
||||||
|
Automatically summarize Discord server conversations and post digests to a dedicated channel. Never miss important discussions again.
|
||||||
|
|
||||||
|
## Two Ways to Use
|
||||||
|
|
||||||
|
### 🤖 Option 1: Clawdbot Skill (Recommended if you use Clawdbot)
|
||||||
|
|
||||||
|
Zero code required. Just configure cron jobs and let Clawdbot handle the rest.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- No separate bot to host
|
||||||
|
- Uses your existing Clawdbot setup
|
||||||
|
- AI summarization built-in
|
||||||
|
- Easy to customize prompts
|
||||||
|
|
||||||
|
**Setup:** See [`skill/SKILL.md`](skill/SKILL.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🐍 Option 2: Standalone Bot
|
||||||
|
|
||||||
|
A self-contained Python Discord bot. Deploy anywhere.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Works without Clawdbot
|
||||||
|
- Full control over deployment
|
||||||
|
- Docker-ready
|
||||||
|
- Manual trigger commands
|
||||||
|
|
||||||
|
**Setup:** See [`standalone/README.md`](standalone/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Collect** - Reads messages from all accessible channels since last summary
|
||||||
|
2. **Summarize** - Uses Claude/GPT to create a concise digest
|
||||||
|
3. **Post** - Sends the summary to your #tldr channel
|
||||||
|
4. **Repeat** - Runs on schedule (default: 6 AM, 1 PM, 10 PM)
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
📋 **TLDR Summary** (Jan 25, 2026 - 1:00 PM)
|
||||||
|
|
||||||
|
Busy morning! Major progress on the API, some hiring discussions,
|
||||||
|
and the eternal tabs-vs-spaces debate resurfaces.
|
||||||
|
|
||||||
|
**#general**
|
||||||
|
• Welcomed @newbie to the team
|
||||||
|
• Q1 roadmap discussion - focusing on mobile first
|
||||||
|
• Friday team lunch @ 12:30
|
||||||
|
|
||||||
|
**#engineering**
|
||||||
|
• Shipped v2.1.0 to staging
|
||||||
|
• Found and fixed the auth token bug
|
||||||
|
• Code review backlog cleared
|
||||||
|
|
||||||
|
**#hiring**
|
||||||
|
• 3 new senior eng candidates in pipeline
|
||||||
|
• Technical interview scheduled for Thursday
|
||||||
|
|
||||||
|
**Action Items:**
|
||||||
|
- [ ] @alice: Finalize API docs by Wednesday
|
||||||
|
- [ ] @bob: Set up staging environment monitoring
|
||||||
|
- [ ] Everyone: Submit Q1 goals by EOW
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
- **Async-friendly**: Not everyone can keep up with Discord in real-time
|
||||||
|
- **Context preservation**: Important decisions don't get buried
|
||||||
|
- **Onboarding**: New members can quickly catch up on team dynamics
|
||||||
|
- **Documentation**: Creates a lightweight log of team activity
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
PRs welcome! Ideas for improvement:
|
||||||
|
- Slack/Teams support
|
||||||
|
- Weekly digest mode
|
||||||
|
- Thread summarization
|
||||||
|
- Sentiment analysis
|
||||||
|
- Custom summary templates
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT - do whatever you want with it
|
||||||
111
skill/SKILL.md
Normal file
111
skill/SKILL.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Discord TLDR Skill
|
||||||
|
|
||||||
|
Automatically summarize Discord server conversations and post digests to a dedicated #tldr channel.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill configures scheduled summaries of your Discord server's activity. Perfect for:
|
||||||
|
- Teams who want async-friendly catch-up
|
||||||
|
- Servers with multiple active channels
|
||||||
|
- Anyone who doesn't want to scroll through hundreds of messages
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Clawdbot with Discord channel configured
|
||||||
|
- Bot must have read access to channels you want summarized
|
||||||
|
- A dedicated #tldr channel for posting summaries
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Create your #tldr channel
|
||||||
|
|
||||||
|
Create a text channel in your Discord server (e.g., `#tldr` or `#daily-digest`). Note the channel ID.
|
||||||
|
|
||||||
|
### 2. Get your Guild ID
|
||||||
|
|
||||||
|
Your Discord server's guild ID. You can get this by enabling Developer Mode in Discord, then right-clicking your server name.
|
||||||
|
|
||||||
|
### 3. Add the cron jobs
|
||||||
|
|
||||||
|
Run these commands in Clawdbot or add to your config:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Morning summary (6 AM)
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "tldr-morning" \
|
||||||
|
--schedule "0 6 * * *" \
|
||||||
|
--tz "America/New_York" \
|
||||||
|
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||||
|
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||||
|
--channel discord
|
||||||
|
|
||||||
|
# Afternoon summary (1 PM)
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "tldr-afternoon" \
|
||||||
|
--schedule "0 13 * * *" \
|
||||||
|
--tz "America/New_York" \
|
||||||
|
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||||
|
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||||
|
--channel discord
|
||||||
|
|
||||||
|
# Night summary (10 PM)
|
||||||
|
clawdbot cron add \
|
||||||
|
--name "tldr-night" \
|
||||||
|
--schedule "0 22 * * *" \
|
||||||
|
--tz "America/New_York" \
|
||||||
|
--message "Read all recent messages from the Discord server (guild YOUR_GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id YOUR_TLDR_CHANNEL_ID)." \
|
||||||
|
--to "channel:YOUR_TLDR_CHANNEL_ID" \
|
||||||
|
--channel discord
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Customize (optional)
|
||||||
|
|
||||||
|
**Change frequency:** Modify the cron expressions:
|
||||||
|
- `0 9 * * *` = 9 AM daily
|
||||||
|
- `0 */6 * * *` = every 6 hours
|
||||||
|
- `0 9 * * 1-5` = 9 AM weekdays only
|
||||||
|
|
||||||
|
**Change timezone:** Replace `America/New_York` with your timezone.
|
||||||
|
|
||||||
|
**Customize the prompt:** Edit the message to:
|
||||||
|
- Exclude certain channels
|
||||||
|
- Focus on specific topics
|
||||||
|
- Change summary format (bullets, paragraphs, etc.)
|
||||||
|
- Add action item extraction
|
||||||
|
- Tag specific roles for important updates
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
📋 **TLDR Summary** (Jan 25, 2026 - Morning)
|
||||||
|
|
||||||
|
**#general**
|
||||||
|
• Team discussed jacket-making skills and the legend of Nicholai's sibling
|
||||||
|
• Buba wrote an epic poem about said jacket (instant classic)
|
||||||
|
|
||||||
|
**#dev**
|
||||||
|
• Merged PR #42 for the new API endpoint
|
||||||
|
• Blocked on auth token refresh issue - @jake investigating
|
||||||
|
|
||||||
|
**#random**
|
||||||
|
• Heated debate about best pizza toppings (pineapple discourse continues)
|
||||||
|
|
||||||
|
**Action Items:**
|
||||||
|
- [ ] Jake: Fix token refresh bug
|
||||||
|
- [ ] Team: Review Q1 roadmap by Friday
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Summaries not posting?**
|
||||||
|
- Check that Clawdbot has message read permissions in all channels
|
||||||
|
- Verify the tldr channel ID is correct
|
||||||
|
- Run `clawdbot cron list` to confirm jobs are enabled
|
||||||
|
|
||||||
|
**Summaries missing channels?**
|
||||||
|
- Bot needs explicit read access to private channels
|
||||||
|
- Some channels may be excluded by Discord permissions
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT - do whatever you want with it
|
||||||
52
skill/cron-config.json
Normal file
52
skill/cron-config.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"jobs": [
|
||||||
|
{
|
||||||
|
"name": "tldr-morning",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "cron",
|
||||||
|
"expr": "0 6 * * *",
|
||||||
|
"tz": "America/New_York"
|
||||||
|
},
|
||||||
|
"sessionTarget": "isolated",
|
||||||
|
"wakeMode": "next-heartbeat",
|
||||||
|
"payload": {
|
||||||
|
"kind": "agentTurn",
|
||||||
|
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||||
|
"to": "channel:TLDR_CHANNEL_ID",
|
||||||
|
"channel": "discord"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tldr-afternoon",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "cron",
|
||||||
|
"expr": "0 13 * * *",
|
||||||
|
"tz": "America/New_York"
|
||||||
|
},
|
||||||
|
"sessionTarget": "isolated",
|
||||||
|
"wakeMode": "next-heartbeat",
|
||||||
|
"payload": {
|
||||||
|
"kind": "agentTurn",
|
||||||
|
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||||
|
"to": "channel:TLDR_CHANNEL_ID",
|
||||||
|
"channel": "discord"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tldr-night",
|
||||||
|
"schedule": {
|
||||||
|
"kind": "cron",
|
||||||
|
"expr": "0 22 * * *",
|
||||||
|
"tz": "America/New_York"
|
||||||
|
},
|
||||||
|
"sessionTarget": "isolated",
|
||||||
|
"wakeMode": "next-heartbeat",
|
||||||
|
"payload": {
|
||||||
|
"kind": "agentTurn",
|
||||||
|
"message": "Read all recent messages from the Discord server (guild GUILD_ID) across all channels since the last TLDR summary. Create a concise bullet-point summary of key discussions, decisions, ideas, and action items. Post the summary to the #tldr channel (channel id TLDR_CHANNEL_ID).",
|
||||||
|
"to": "channel:TLDR_CHANNEL_ID",
|
||||||
|
"channel": "discord"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
standalone/.env.example
Normal file
18
standalone/.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Discord Configuration
|
||||||
|
DISCORD_TOKEN=your_discord_bot_token_here
|
||||||
|
DISCORD_GUILD_ID=123456789012345678
|
||||||
|
DISCORD_TLDR_CHANNEL_ID=123456789012345678
|
||||||
|
|
||||||
|
# LLM Provider (anthropic or openai)
|
||||||
|
LLM_PROVIDER=anthropic
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||||
|
# OPENAI_API_KEY=sk-xxxxx
|
||||||
|
|
||||||
|
# Schedule (24h format, comma-separated)
|
||||||
|
SUMMARY_HOURS=6,13,22
|
||||||
|
|
||||||
|
# How far back to look for messages (hours)
|
||||||
|
LOOKBACK_HOURS=8
|
||||||
|
|
||||||
|
# Channels to exclude (comma-separated channel IDs)
|
||||||
|
# EXCLUDE_CHANNELS=123456789,987654321
|
||||||
10
standalone/Dockerfile
Normal file
10
standalone/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY discord_tldr.py .
|
||||||
|
|
||||||
|
CMD ["python", "discord_tldr.py"]
|
||||||
126
standalone/README.md
Normal file
126
standalone/README.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Discord TLDR Bot (Standalone)
|
||||||
|
|
||||||
|
A standalone Discord bot that automatically summarizes server conversations and posts digests to a dedicated channel.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📋 Automatic scheduled summaries (configurable times)
|
||||||
|
- 🤖 AI-powered summarization (Claude or GPT)
|
||||||
|
- 📊 Channel-by-channel breakdown
|
||||||
|
- ⚡ Manual trigger via `!tldr` command
|
||||||
|
- 🐳 Docker-ready for easy deployment
|
||||||
|
- 🔒 Respects channel permissions
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create a Discord Bot
|
||||||
|
|
||||||
|
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
|
||||||
|
2. Create a new application
|
||||||
|
3. Go to Bot → Add Bot
|
||||||
|
4. Enable these Privileged Intents:
|
||||||
|
- Message Content Intent
|
||||||
|
- Server Members Intent (optional)
|
||||||
|
5. Copy the bot token
|
||||||
|
|
||||||
|
### 2. Invite the Bot
|
||||||
|
|
||||||
|
Generate an invite URL with these permissions:
|
||||||
|
- Read Messages/View Channels
|
||||||
|
- Send Messages
|
||||||
|
- Read Message History
|
||||||
|
|
||||||
|
```
|
||||||
|
https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=68608&scope=bot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your values
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Run
|
||||||
|
|
||||||
|
**Direct:**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python discord_tldr.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
```bash
|
||||||
|
docker build -t discord-tldr .
|
||||||
|
docker run --env-file .env discord-tldr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Compose:**
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
tldr:
|
||||||
|
build: .
|
||||||
|
env_file: .env
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `!tldr` | Generate summary now (uses default lookback) |
|
||||||
|
| `!tldr 24` | Generate summary for last 24 hours |
|
||||||
|
| `!tldr-status` | Check bot status and schedule |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DISCORD_TOKEN` | required | Bot token |
|
||||||
|
| `DISCORD_GUILD_ID` | required | Server ID |
|
||||||
|
| `DISCORD_TLDR_CHANNEL_ID` | required | Channel for summaries |
|
||||||
|
| `LLM_PROVIDER` | `anthropic` | `anthropic` or `openai` |
|
||||||
|
| `ANTHROPIC_API_KEY` | - | Claude API key |
|
||||||
|
| `OPENAI_API_KEY` | - | GPT API key |
|
||||||
|
| `SUMMARY_HOURS` | `6,13,22` | Hours to post (24h format) |
|
||||||
|
| `LOOKBACK_HOURS` | `8` | Hours to look back |
|
||||||
|
| `EXCLUDE_CHANNELS` | - | Channel IDs to skip |
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
📋 **TLDR Summary** (Jan 25, 2026 - 6:00 AM)
|
||||||
|
|
||||||
|
Quick catch-up: Active discussions about the new API launch and some
|
||||||
|
spirited debate about code review practices.
|
||||||
|
|
||||||
|
**#general**
|
||||||
|
• Team welcomed new member @sarah
|
||||||
|
• Discussion about Q1 roadmap priorities
|
||||||
|
• Decided to postpone the demo to next week
|
||||||
|
|
||||||
|
**#dev**
|
||||||
|
• Merged the authentication refactor (PR #142)
|
||||||
|
• Debugging session for the webhook timeout issue
|
||||||
|
• @jake will investigate the memory leak
|
||||||
|
|
||||||
|
**#random**
|
||||||
|
• Friday lunch plans: Thai food won the vote
|
||||||
|
• Shared some quality memes about merge conflicts
|
||||||
|
|
||||||
|
**Action Items:**
|
||||||
|
- @jake: Investigate memory leak by EOD Monday
|
||||||
|
- @team: Review roadmap doc before Wednesday standup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hosting Options
|
||||||
|
|
||||||
|
- **VPS**: Any cheap VPS (DigitalOcean, Linode, Vultr)
|
||||||
|
- **Railway/Render**: Easy container deployment
|
||||||
|
- **Home server**: Run in Docker with auto-restart
|
||||||
|
- **Raspberry Pi**: Works great for small servers
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
299
standalone/discord_tldr.py
Normal file
299
standalone/discord_tldr.py
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Discord TLDR Bot
|
||||||
|
Summarizes Discord server conversations and posts to a #tldr channel.
|
||||||
|
|
||||||
|
Standalone version - no Clawdbot required.
|
||||||
|
Uses Discord.py for Discord API and Anthropic/OpenAI for summarization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands, tasks
|
||||||
|
|
||||||
|
# Optional: use anthropic or openai for summarization
|
||||||
|
try:
|
||||||
|
import anthropic
|
||||||
|
HAS_ANTHROPIC = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ANTHROPIC = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import openai
|
||||||
|
HAS_OPENAI = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_OPENAI = False
|
||||||
|
|
||||||
|
# Configuration (override with environment variables)
|
||||||
|
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
||||||
|
GUILD_ID = int(os.getenv("DISCORD_GUILD_ID", "0"))
|
||||||
|
TLDR_CHANNEL_ID = int(os.getenv("DISCORD_TLDR_CHANNEL_ID", "0"))
|
||||||
|
|
||||||
|
# LLM Configuration
|
||||||
|
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "anthropic") # "anthropic" or "openai"
|
||||||
|
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
|
||||||
|
# Summary schedule (hours in 24h format, comma-separated)
|
||||||
|
SUMMARY_HOURS = [int(h) for h in os.getenv("SUMMARY_HOURS", "6,13,22").split(",")]
|
||||||
|
|
||||||
|
# Channels to exclude (comma-separated channel IDs)
|
||||||
|
EXCLUDE_CHANNELS = [int(c) for c in os.getenv("EXCLUDE_CHANNELS", "").split(",") if c]
|
||||||
|
|
||||||
|
# How far back to look for messages (hours)
|
||||||
|
LOOKBACK_HOURS = int(os.getenv("LOOKBACK_HOURS", "8"))
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("discord-tldr")
|
||||||
|
|
||||||
|
# Bot setup
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.guilds = True
|
||||||
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageCollector:
|
||||||
|
"""Collects messages from Discord channels."""
|
||||||
|
|
||||||
|
def __init__(self, guild: discord.Guild, exclude_channels: list[int]):
|
||||||
|
self.guild = guild
|
||||||
|
self.exclude_channels = exclude_channels
|
||||||
|
|
||||||
|
async def collect_messages(self, since: datetime) -> dict[str, list[dict]]:
|
||||||
|
"""Collect messages from all text channels since the given time."""
|
||||||
|
messages_by_channel = {}
|
||||||
|
|
||||||
|
for channel in self.guild.text_channels:
|
||||||
|
if channel.id in self.exclude_channels:
|
||||||
|
continue
|
||||||
|
if channel.id == TLDR_CHANNEL_ID:
|
||||||
|
continue # Don't summarize the tldr channel itself
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages = []
|
||||||
|
async for msg in channel.history(after=since, limit=500):
|
||||||
|
if msg.author.bot:
|
||||||
|
continue
|
||||||
|
messages.append({
|
||||||
|
"author": msg.author.display_name,
|
||||||
|
"content": msg.content,
|
||||||
|
"timestamp": msg.created_at.isoformat(),
|
||||||
|
"attachments": len(msg.attachments),
|
||||||
|
"reactions": sum(r.count for r in msg.reactions) if msg.reactions else 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
messages_by_channel[channel.name] = list(reversed(messages))
|
||||||
|
logger.info(f"Collected {len(messages)} messages from #{channel.name}")
|
||||||
|
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(f"No access to #{channel.name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error collecting from #{channel.name}: {e}")
|
||||||
|
|
||||||
|
return messages_by_channel
|
||||||
|
|
||||||
|
|
||||||
|
class Summarizer:
|
||||||
|
"""Summarizes messages using an LLM."""
|
||||||
|
|
||||||
|
def __init__(self, provider: str = "anthropic"):
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
if provider == "anthropic" and HAS_ANTHROPIC:
|
||||||
|
self.client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
|
||||||
|
elif provider == "openai" and HAS_OPENAI:
|
||||||
|
self.client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
||||||
|
else:
|
||||||
|
self.client = None
|
||||||
|
logger.warning(f"LLM provider '{provider}' not available, using basic summarization")
|
||||||
|
|
||||||
|
def _format_messages_for_prompt(self, messages_by_channel: dict) -> str:
|
||||||
|
"""Format collected messages into a prompt-friendly string."""
|
||||||
|
parts = []
|
||||||
|
for channel, messages in messages_by_channel.items():
|
||||||
|
parts.append(f"\n## #{channel}")
|
||||||
|
for msg in messages:
|
||||||
|
parts.append(f"[{msg['author']}]: {msg['content']}")
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
async def summarize(self, messages_by_channel: dict) -> str:
|
||||||
|
"""Generate a summary of the messages."""
|
||||||
|
if not messages_by_channel:
|
||||||
|
return "No new messages to summarize."
|
||||||
|
|
||||||
|
formatted = self._format_messages_for_prompt(messages_by_channel)
|
||||||
|
|
||||||
|
prompt = f"""Summarize the following Discord server conversations into a concise TLDR digest.
|
||||||
|
|
||||||
|
Format your response as:
|
||||||
|
1. A brief overview (1-2 sentences)
|
||||||
|
2. Bullet points organized by channel, highlighting:
|
||||||
|
- Key discussions and decisions
|
||||||
|
- Important ideas or proposals
|
||||||
|
- Action items or next steps
|
||||||
|
- Notable moments or highlights
|
||||||
|
|
||||||
|
Keep it scannable and useful for someone catching up.
|
||||||
|
|
||||||
|
---
|
||||||
|
MESSAGES:
|
||||||
|
{formatted}
|
||||||
|
---
|
||||||
|
|
||||||
|
Generate the TLDR summary:"""
|
||||||
|
|
||||||
|
if self.provider == "anthropic" and self.client:
|
||||||
|
response = self.client.messages.create(
|
||||||
|
model="claude-sonnet-4-20250514",
|
||||||
|
max_tokens=1500,
|
||||||
|
messages=[{"role": "user", "content": prompt}]
|
||||||
|
)
|
||||||
|
return response.content[0].text
|
||||||
|
|
||||||
|
elif self.provider == "openai" and self.client:
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model="gpt-4o",
|
||||||
|
max_tokens=1500,
|
||||||
|
messages=[{"role": "user", "content": prompt}]
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Basic fallback: just list message counts
|
||||||
|
lines = ["📋 **Activity Summary**\n"]
|
||||||
|
for channel, messages in messages_by_channel.items():
|
||||||
|
lines.append(f"• **#{channel}**: {len(messages)} messages")
|
||||||
|
lines.append("\n*Install anthropic or openai package for AI summaries*")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
class TLDRBot:
|
||||||
|
"""Main bot class that orchestrates collection and summarization."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.collector = None
|
||||||
|
self.summarizer = Summarizer(LLM_PROVIDER)
|
||||||
|
self.last_summary_time = None
|
||||||
|
|
||||||
|
async def generate_and_post_summary(self):
|
||||||
|
"""Generate a summary and post it to the TLDR channel."""
|
||||||
|
guild = bot.get_guild(GUILD_ID)
|
||||||
|
if not guild:
|
||||||
|
logger.error(f"Guild {GUILD_ID} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
tldr_channel = guild.get_channel(TLDR_CHANNEL_ID)
|
||||||
|
if not tldr_channel:
|
||||||
|
logger.error(f"TLDR channel {TLDR_CHANNEL_ID} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine lookback time
|
||||||
|
if self.last_summary_time:
|
||||||
|
since = self.last_summary_time
|
||||||
|
else:
|
||||||
|
since = datetime.now(timezone.utc) - timedelta(hours=LOOKBACK_HOURS)
|
||||||
|
|
||||||
|
# Collect messages
|
||||||
|
self.collector = MessageCollector(guild, EXCLUDE_CHANNELS)
|
||||||
|
messages = await self.collector.collect_messages(since)
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
logger.info("No new messages to summarize")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
summary = await self.summarizer.summarize(messages)
|
||||||
|
|
||||||
|
# Format and post
|
||||||
|
now = datetime.now()
|
||||||
|
header = f"📋 **TLDR Summary** ({now.strftime('%b %d, %Y - %I:%M %p')})\n\n"
|
||||||
|
|
||||||
|
# Discord has a 2000 char limit
|
||||||
|
full_message = header + summary
|
||||||
|
if len(full_message) > 2000:
|
||||||
|
# Split into multiple messages
|
||||||
|
chunks = [full_message[i:i+1990] for i in range(0, len(full_message), 1990)]
|
||||||
|
for chunk in chunks:
|
||||||
|
await tldr_channel.send(chunk)
|
||||||
|
else:
|
||||||
|
await tldr_channel.send(full_message)
|
||||||
|
|
||||||
|
self.last_summary_time = datetime.now(timezone.utc)
|
||||||
|
logger.info("Posted TLDR summary")
|
||||||
|
|
||||||
|
|
||||||
|
tldr_bot = TLDRBot()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
logger.info(f"Logged in as {bot.user}")
|
||||||
|
check_summary_time.start()
|
||||||
|
|
||||||
|
|
||||||
|
@tasks.loop(minutes=1)
|
||||||
|
async def check_summary_time():
|
||||||
|
"""Check if it's time to post a summary."""
|
||||||
|
now = datetime.now()
|
||||||
|
if now.hour in SUMMARY_HOURS and now.minute == 0:
|
||||||
|
await tldr_bot.generate_and_post_summary()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command(name="tldr")
|
||||||
|
async def manual_tldr(ctx, hours: int = None):
|
||||||
|
"""Manually trigger a TLDR summary. Usage: !tldr [hours]"""
|
||||||
|
if hours:
|
||||||
|
tldr_bot.last_summary_time = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||||
|
await ctx.send("Generating TLDR summary...")
|
||||||
|
await tldr_bot.generate_and_post_summary()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command(name="tldr-status")
|
||||||
|
async def tldr_status(ctx):
|
||||||
|
"""Check TLDR bot status."""
|
||||||
|
last = tldr_bot.last_summary_time
|
||||||
|
last_str = last.strftime('%Y-%m-%d %H:%M UTC') if last else "Never"
|
||||||
|
schedule = ", ".join(f"{h}:00" for h in SUMMARY_HOURS)
|
||||||
|
|
||||||
|
await ctx.send(
|
||||||
|
f"**TLDR Bot Status**\n"
|
||||||
|
f"• Last summary: {last_str}\n"
|
||||||
|
f"• Schedule: {schedule}\n"
|
||||||
|
f"• Lookback: {LOOKBACK_HOURS} hours\n"
|
||||||
|
f"• LLM Provider: {LLM_PROVIDER}\n"
|
||||||
|
f"• Excluded channels: {len(EXCLUDE_CHANNELS)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not DISCORD_TOKEN:
|
||||||
|
print("Error: DISCORD_TOKEN environment variable not set")
|
||||||
|
print("\nRequired environment variables:")
|
||||||
|
print(" DISCORD_TOKEN - Your Discord bot token")
|
||||||
|
print(" DISCORD_GUILD_ID - Your server's guild ID")
|
||||||
|
print(" DISCORD_TLDR_CHANNEL_ID - Channel ID for posting summaries")
|
||||||
|
print("\nOptional:")
|
||||||
|
print(" LLM_PROVIDER - 'anthropic' or 'openai' (default: anthropic)")
|
||||||
|
print(" ANTHROPIC_API_KEY - Anthropic API key (if using Claude)")
|
||||||
|
print(" OPENAI_API_KEY - OpenAI API key (if using GPT)")
|
||||||
|
print(" SUMMARY_HOURS - Comma-separated hours (default: 6,13,22)")
|
||||||
|
print(" LOOKBACK_HOURS - Hours to look back (default: 8)")
|
||||||
|
print(" EXCLUDE_CHANNELS - Comma-separated channel IDs to skip")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not GUILD_ID or not TLDR_CHANNEL_ID:
|
||||||
|
print("Error: DISCORD_GUILD_ID and DISCORD_TLDR_CHANNEL_ID must be set")
|
||||||
|
return
|
||||||
|
|
||||||
|
bot.run(DISCORD_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
standalone/requirements.txt
Normal file
9
standalone/requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Core
|
||||||
|
discord.py>=2.3.0
|
||||||
|
|
||||||
|
# LLM providers (install at least one)
|
||||||
|
anthropic>=0.18.0
|
||||||
|
openai>=1.12.0
|
||||||
|
|
||||||
|
# Optional: for better async handling
|
||||||
|
aiohttp>=3.9.0
|
||||||
Loading…
x
Reference in New Issue
Block a user