A Telegram bot that collects your chat messages and stores them in Google Drive for building your personal second brain knowledge base.
This bot acts as your personal data collector:
- Listens to messages you send in Telegram
- Collects and structures your chat data
- Uploads data to your Google Drive
- Data is later processed by specialized services for filtering and organization
Think of it as the first step in building your second brain - capturing everything you communicate in Telegram.
Functional: The bot has a working webhook infrastructure, Google OAuth 2.0 authentication, encrypted token storage in PostgreSQL (Supabase), and Google Drive integration for saving and editing messages as markdown files. It also offers /timebox — an LLM (OpenRouter) turns your next-day tasks into a timeboxed schedule, optionally published to a dedicated Google Calendar.
- Python 3.10 or higher (the codebase uses
X | Nonetype unions; the Docker image is built on 3.11) - Telegram account
- Google account with Drive and Calendar API access
- Telegram bot token from @BotFather
- OpenRouter API key (required — used by
/timebox)
- Open Telegram and message
@BotFather - Send
/newbotand follow the instructions - Save the bot token you receive
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # On macOS/Linux
# venv\Scripts\activate # On Windows
# Install required packages
pip install -r requirements.txt# Copy example configuration
cp .env.example .env
# Edit .env with your credentialsYour .env file should contain:
TELEGRAM_BOT_TOKEN=your_bot_token_here
LOG_LEVEL=INFOgo to DEVELOPMENT.md for local development testing. go to DEPLOY.md for remote deployment.
/start- Initialize conversation with the bot/help- Get help information/authenticate- Connect your Google Drive via OAuth 2.0/timebox- Turn next-day tasks into a timeboxed schedule (optionally written to Google Calendar)/status- Check authentication and Drive connection status/logout- Disconnect Google Drive and remove stored tokens
Once authenticated, send any text message and it gets saved to a daily markdown file in your Google Drive. Edit a message in Telegram and the Drive file updates automatically.
POST /api/send-message lets a trusted caller make the bot send a Telegram message. Disabled unless OUTBOUND_API_SECRET is set.
curl -X POST https://your-domain.com/api/send-message \
-H "Authorization: Bearer $OUTBOUND_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"chat_id": 123456789, "text": "hello from the bot"}'Body fields: chat_id (int or string, required), text (string, required, ≤4096 chars), parse_mode (optional: Markdown, MarkdownV2, or HTML). Returns {"ok": true, "message_id": <int>} on success.
second_brain_bot/
├── src/
│ ├── bot.py # Command handlers (/start, /help, /authenticate, /timebox, /status, /logout) + register_handlers
│ ├── webhook_server.py # Flask server: Telegram webhook, OAuth callback, outbound /api/send-message
│ ├── config.py # Configuration and environment management
│ ├── google_auth.py # OAuth 2.0 flow (drive.file + calendar.events), token storage (PostgreSQL), CSRF protection
│ ├── drive_handler.py # Google Drive API: file creation, message append/edit
│ ├── timebox.py # /timebox conversation: collect next-day tasks, build & publish a schedule
│ ├── scheduler.py # Timebox scheduling: target-date, LLM generation, rendering
│ └── calendar_handler.py # Google Calendar writes for the schedule (tag/list/clear/per-item)
├── k8s/ # Kubernetes manifests
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose deployment
├── requirements.txt # Python dependencies
├── .env # Your credentials (create from .env.example)
├── .env.example # Template for configuration
├── README.md # This file (user guide)
├── DEVELOPMENT.md # Local development guide
├── DEPLOY.md # Docker/Kubernetes deployment guide
└── CLAUDE.md # Technical documentation for AI agents
TELEGRAM_BOT_TOKEN(required) - Your bot token from @BotFatherWEBHOOK_URL(required) - Public HTTPS base URL (e.g.https://your-domain.com)WEBHOOK_PORT(optional, default8443) - Port for webhook server (80, 88, 443, or 8443)WEBHOOK_PATH(optional, default/webhook) - Path prefix for the webhook endpointGOOGLE_CLIENT_ID(required) - OAuth client ID from Google Cloud ConsoleGOOGLE_CLIENT_SECRET(required) - OAuth client secretGOOGLE_REDIRECT_URI(optional) - OAuth callback URL (defaults to{WEBHOOK_URL}/oauth/callback)DATABASE_USER/DATABASE_PASSWORD/DATABASE_HOST/DATABASE_PORT/DATABASE_NAME(required) - PostgreSQL connection detailsTOKEN_ENCRYPTION_KEY(required) - Fernet key for encrypting stored OAuth tokensDRIVE_FOLDER_NAME(optional, defaultsecond_brain_bot/) - Name of the Drive folder the bot creates; daily notes are stored inside it asYYYY-MM-DD.mdDAY_CUTOFF_HOUR(optional, default0) - Hour (0-23) before which messages are filed under the previous day.0disables it (midnight boundary)OUTBOUND_API_SECRET(optional) - Shared secret forPOST /api/send-message; leave unset to disable (returns 503). Generate withopenssl rand -hex 32.LOG_LEVEL(optional, defaultINFO) - Logging verbosity: DEBUG, INFO, WARNING, ERROR, CRITICAL
OPENROUTER_API_KEY(required) - OpenRouter API key for schedule generation. Startup fails without it (get one)TIMEBOX_LLM_MODEL(optional, defaultdeepseek/deepseek-v4-flash) - OpenRouter model used for schedulingTIMEBOX_TIMEZONE(optional, defaultAsia/Singapore) - IANA timezone for computing the target dayTIMEBOX_CUTOFF_HOUR(optional, default3) - Sessions finishing before this local hour plan the current day instead of tomorrowTIMEBOX_CALENDAR_ID(optional) - Calendar ID of a dedicated calendar to publish the schedule into. Neverprimary. Unset → schedule is replied as text onlyTIMEBOX_DAY_START/TIMEBOX_DAY_END/TIMEBOX_LUNCH/TIMEBOX_DINNER/TIMEBOX_EAT_DURATION/TIMEBOX_COMMUTE_MORNING/TIMEBOX_COMMUTE_EVENING/TIMEBOX_COMMUTE_DURATION(optional) - Fixed daily anchors handed to the planner as overridable defaults. See.env.examplefor defaults
- Create a project in Google Cloud Console
- Enable the Google Drive API and the Google Calendar API (the latter is needed for
/timeboxcalendar publishing) - Create OAuth 2.0 credentials (Web application type)
- Add your redirect URI (e.g.
https://your-domain.com/oauth/callback) - Copy the client ID and client secret to your
.env
The bot requests the drive.file and calendar.events scopes. If you authenticated before calendar support was added, re-run /authenticate to grant the new scope.
- Create a project at Supabase
- Run this migration in the SQL Editor:
CREATE TABLE user_tokens ( user_id BIGINT PRIMARY KEY, encrypted_token TEXT NOT NULL, token_expires_at TIMESTAMP, last_accessed TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW() );
- Copy the database connection details to your
.env
Generate a Fernet key and add it to your .env:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"- Never commit your
.envfile - Keep your bot token private
- Don't share OAuth credentials
- The
.gitignoreexcludes sensitive files automatically
The OAuth CSRF state cache (_state_cache in google_auth.py) currently lives in-memory. If the process restarts between a user clicking /authenticate and completing the Google consent screen, the callback fails. This also prevents running multiple instances.
Option A: External KV store (e.g. Cloudflare KV)
- OAuth state is short-lived (10 min TTL) — KV supports TTL natively
- Access pattern is single-key lookup — KV's strongest use case
- Low volume (only during auth, not per-message)
Option B: Signed state (no storage needed)
- Encode
user_idandexpires_atinto the state string, sign it withTOKEN_ENCRYPTION_KEYusing HMAC - The callback validates the signature and expiry without any storage lookup
- Tradeoff: loses one-time-use enforcement, but Google's auth code is already single-use so the practical risk is minimal
Currently every message triggers a download-append-upload cycle (2 Drive API calls). For bursts of messages this is wasteful and creates potential race conditions.
Option A: Time-window buffer
- Collect messages in a buffer (in-memory, KV, or Redis)
- Flush to Drive after N seconds of inactivity or when buffer hits a size threshold
- Single download + append all + upload per flush
- Risk: messages lost if the process dies mid-buffer; mitigate by buffering in durable storage
Option B: Pending messages table (more robust)
- Write each message immediately to a
pending_messagestable in PostgreSQL - A periodic job (every 30-60s) collects all pending messages per user, downloads the Drive file once, appends all, uploads, then marks them as synced
- Edits update the pending row if not yet synced, or trigger a Drive update if already synced
- Decouples Telegram response time from Drive API latency — the bot can acknowledge instantly
Currently only text messages are handled. Supporting media requires a different storage strategy since binary files can't live inline in markdown.
Approach:
- Upload media files to a subfolder in the user's Google Drive (e.g.
SecondBrain/media/) - Reference them in the markdown by Drive link:
<!-- msg_id: 456 --> [Photo: sunset.jpg](https://drive.google.com/file/d/abc123/view) Caption text here
Considerations:
- Telegram file download:
await context.bot.get_file(file_id)then.download_as_bytearray(). Telegram stores files temporarily, so download promptly - File size limits: Telegram Bot API caps file downloads at 20MB
- Drive upload:
MediaInMemoryUploadfor small files,MediaIoBaseUploadwith streaming for larger ones - Handler changes: Broaden filters to include
filters.PHOTO,filters.VIDEO,filters.AUDIO,filters.Document.ALL. Each type exposes file IDs differently (message.photo[-1].file_idfor highest-res photo,message.document.file_idfor documents, etc.) - Downstream compatibility: The markdown format should match what the second brain processing service expects — simple Drive links are the most portable option
- Batching — most immediate reliability and performance gain
- Stateless — matters when deploying multiple instances or moving to serverless
- Media support — biggest feature expansion, most implementation work
See CLAUDE.md for technical details, architecture decisions, and implementation roadmap.
MIT License
Built for capturing ideas and building a personal knowledge base, one message at a time.