Architecture
Solstice uses a hybrid dual-process architecture - a Go engine handles all Telegram Bot API interactions, command parsing, database operations, and queue management, while a Python daemon manages the actual voice chat streaming via PyTgCalls/Pyrogram.
High-Level Overview
┌─────────────────────────────────────────────────────────────────┐
│ Telegram Cloud │
│ (Bot API + MTProto API) │
└──────────────┬──────────────────────────────────┬───────────────┘
│ Bot API (HTTPS) │ MTProto (TCP)
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ Go Engine (bot) │ │ Python Daemon (vc_daemon.py) │
│ │ │ │
│ • Command parsing │ │ • Pyrogram MTProto client │
│ • User authentication │ │ • PyTgCalls voice chat engine │
│ • Queue management │ │ • FFmpeg audio/video pipeline │
│ • YouTube search/extract│ │ • Stream lifecycle management │
│ • MongoDB persistence │ │ • HTTP API server (:5050) │
│ • i18n string rendering │ │ │
│ • Admin/sudo logic │ │ │
│ • HTTP client (:5051) │ │ │
└────────────┬─────────────┘ └──────────────┬───────────────────┘
│ │
│ JSON-over-HTTP (localhost) │
│◄────────────────────────────────►│
│ │
│ Go → Python :5050 │
│ Python → Go :5051 │
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────────┐
│ MongoDB │ │ Voice Chat │
│ (persistent state) │ │ (Telegram Group VC) │
└──────────────────────────┘ └──────────────────────────────────┘Why Two Languages?
| Concern | Go | Python |
|---|---|---|
| Bot API command handling | Native telebot.v3 | - |
| Concurrency & speed | Goroutines, channels | - |
| Voice chat streaming | - | PyTgCalls (only available in Python) |
| MTProto userbot session | - | Pyrogram |
| YouTube extraction | yt-dlp subprocess | Used as library in search |
| Database access | go.mongodb.org/mongo-driver | - |
The PyTgCalls library - which provides the low-level interface to Telegram's voice chat protocol - is only available for Python. This constraint drives the hybrid architecture. Go handles everything else because of its superior concurrency model and compile-time safety.
Inter-Process Communication (IPC)
The two processes communicate over localhost HTTP using simple JSON payloads. This is intentionally lightweight - no gRPC, no message queues - just plain HTTP POST requests.
Go → Python (port 5050)
The Go engine sends commands to the Python daemon's HTTP server:
| Endpoint | Method | Payload | Description |
|---|---|---|---|
/play | POST | {"chat_id", "stream_url", "video", "title"} | Start or enqueue a stream |
/pause | POST | {"chat_id"} | Pause current stream |
/resume | POST | {"chat_id"} | Resume paused stream |
/stop | POST | {"chat_id"} | Stop stream and leave VC |
/seek | POST | {"chat_id", "position"} | Seek to position in seconds |
/volume | POST | {"chat_id", "volume"} | Set volume (1-200) |
Python → Go (port 5051)
The Python daemon sends event notifications back to the Go engine:
| Endpoint | Method | Payload | Description |
|---|---|---|---|
/stream_end | POST | {"chat_id"} | Current stream finished, triggers auto-queue advancement |
Sequence: Playing a Song
User sends: /play Bohemian Rhapsody
│
▼
┌─── Go Engine ──────────────────────────────────────────────┐
│ 1. Parse command, authenticate user │
│ 2. Search YouTube (HTML scrape → proxy → yt-dlp fallback) │
│ 3. Extract direct stream URL via yt-dlp │
│ 4. Check Redis cache (Upstash) for cached URL │
│ 5. Add song to per-chat queue (QueueManager) │
│ 6. If queue position == 0, POST /play to Python daemon │
│ 7. Send "Now Playing" card to chat with inline controls │
│ 8. Log play event to logger channel │
└────────────────────────────────────────────────────────────┘
│
│ HTTP POST localhost:5050/play
▼
┌─── Python Daemon ──────────────────────────────────────────┐
│ 1. Receive play request │
│ 2. Join voice chat via Pyrogram (if not already joined) │
│ 3. Start FFmpeg pipeline for audio/video stream │
│ 4. Feed stream to PyTgCalls │
│ 5. When stream ends, POST /stream_end to Go engine │
└────────────────────────────────────────────────────────────┘
│
│ HTTP POST localhost:5051/stream_end
▼
┌─── Go Engine ──────────────────────────────────────────────┐
│ 1. Advance queue: pop current song, peek next │
│ 2. If next song exists, POST /play to Python daemon │
│ 3. If queue empty + autoplay enabled: │
│ a. Fetch related videos via YouTube Mix (RD playlist) │
│ b. Extract stream URL, POST /play │
│ 4. If queue empty + autoplay disabled: │
│ a. POST /stop to Python daemon │
│ b. Send "Queue finished" message │
└────────────────────────────────────────────────────────────┘Concurrency Model
Go Engine
- Goroutines are used extensively for non-blocking operations:
- YouTube search and extraction run in goroutines to avoid blocking the bot's message loop
- Broadcast messages iterate chats in a background goroutine with 100ms delays to avoid Telegram flood limits
- Global ban enforcement runs asynchronously across all served chats
sync.Mutexprotects shared state:QueueManageruses a mutex to safely manage per-chat song queues from concurrent command handlersvoteSkipStoreuses a mutex for thread-safe vote counting- Whisper message storage uses
sync.RWMutex
Python Daemon
asyncioevent loop drives the entire daemon:- PyTgCalls stream callbacks are async
- The HTTP server (
aiohttp) runs on the same event loop - All Pyrogram API calls are awaited
Process Lifecycle
main.go
│
├── Load .env → config.LoadConfig()
├── Load i18n strings (en.yml)
├── Connect to MongoDB → database.InitDB()
├── Load YouTube cookies from DB/env → cookies.txt
├── Load Pyrogram session string
│
├── Create VoiceChatManager
│ └── Spawns vc_daemon.py as subprocess
│ └── Python process starts HTTP server on :5050
│
├── Create BotHandler (telebot.v3)
│ ├── Register all command routes
│ └── Wire up OnStreamEnd callback
│
├── Start bot polling (goroutine)
│ └── Long polling with 10s timeout
│
└── Wait for SIGINT/SIGTERM → graceful shutdown