A lightweight Dockerized service written in Go that monitors Telegram channels and automatically forwards posts (including photos, videos, and documents) to Discord channels via webhooks.
- Monitor multiple Telegram channels simultaneously
- Forward to multiple Discord destinations
- Support for photos, videos, and documents
- Automatic filtering of oversized files (configurable)
- Uses real Telegram user account (no bot limitations)
- Docker-based deployment with a single static binary
- Simple YAML configuration (backward compatible with previous Python version)
- Structured logging
- Session file persistence
- Per-channel settings
- Media-only mode
- Caption removal
- Per-destination file size limits
- Smart image compression (progressive JPEG quality reduction)
- Video compression via ffmpeg
- Album/grouped message support
- Rate-limit handling with Retry-After
- Docker and Docker Compose installed
- Telegram account
- Discord webhook URL(s)
- Telegram API credentials (get from my.telegram.org)
- Go to my.telegram.org
- Sign in with your phone number
- Go to "API development tools"
- Create a new application (any name and description)
- Copy the
api_idandapi_hash
Create a .env file:
# Telegram API credentials
TELEGRAM_API_ID=your_api_id_here
TELEGRAM_API_HASH=your_api_hash_here
# Optional: Session file name (default: media_forwarder)
TELEGRAM_SESSION_NAME=media_forwarder
# Optional: Log level (DEBUG, INFO, WARNING, ERROR)
LOG_LEVEL=INFOCreate a config/channels.yaml file:
channels:
- channel: "@channel_name_to_monitor"
destinations:
- discord_main
discord_webhooks:
discord_main:
url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
settings:
max_file_size_mb: 10
log_level: INFO
include_channel_name: true
include_timestamp: true
group_timeout_seconds: 3.0Run the login command (interactive):
docker compose run --rm media-forwarder loginFollow the prompts:
- Enter your phone number (with country code, e.g., +1234567890)
- Enter the verification code sent to Telegram
- Enter your 2FA password if enabled
The session file will be saved to ./sessions/media_forwarder.session
Check your configuration:
docker compose run --rm media-forwarder validatedocker compose up -ddocker compose logs -f| Command | Description |
|---|---|
login |
Interactive Telegram login |
run |
Start the forwarder (default command) |
validate |
Validate configuration file |
All commands support the --config flag to specify a custom config path:
docker compose run --rm media-forwarder validate --config /path/to/config.yamlChannels can be specified by username or numeric ID:
channels:
# By username
- channel: "@example_channel"
# By numeric ID
- channel: "-1001234567890"Each destination can have its own max file size:
discord_webhooks:
discord_main:
url: "https://discord.com/api/webhooks/..."
max_file_size_mb: 25 # Custom limit for this destinationMax File Size Priority:
- Destination-specific setting (highest priority)
- Channel-specific setting
- Global default setting
channels:
- channel: "@media_channel"
destinations:
- discord_main
settings:
# Only forward messages with media
media_only: true
# Remove captions from media posts
remove_captions: false
# Custom max file size for this channel
max_file_size_mb: 25
# Override metadata inclusion
include_channel_name: true
include_timestamp: trueAvailable Channel Settings:
| Setting | Type | Description |
|---|---|---|
media_only |
bool | Only forward messages with media, skip text-only posts |
remove_captions |
bool | Remove captions from media posts |
max_file_size_mb |
int | Override maximum file size for this channel |
include_channel_name |
bool | Override include channel name setting |
include_timestamp |
bool | Override include timestamp setting |
Note: The
translate_captionssetting is accepted for backward compatibility but has no effect in this version.
Forward a channel to multiple Discord webhooks:
channels:
- channel: "@my_channel"
destinations:
- discord_main
- discord_backupsettings:
# Maximum file size in MB to upload to Discord
max_file_size_mb: 10
# Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
log_level: INFO
# Include channel name in forwarded messages
include_channel_name: true
# Include timestamp in forwarded messages
include_timestamp: true
# Seconds to wait for album messages before forwarding
group_timeout_seconds: 3.0- Checks if file exceeds destination's max file size
- Verifies compression is feasible (file < 200MB, is an image)
- Progressively reduces JPEG quality from 95% to 60%
- Accepts compression only if size reduces by at least 30%
- Sends compressed image if successful, falls back to text-only if not
- Checks if file exceeds destination's max file size
- Verifies compression is feasible (file < 500MB, is a video)
- Requires ffmpeg to be available in the container
- Uses ffmpeg to compress with H.264 codec and AAC audio
- Calculates target bitrate based on desired file size and video duration
- Sends compressed video if successful, falls back to text-only if not
| Parameter | Value |
|---|---|
| Maximum JPEG quality | 95% |
| Minimum JPEG quality | 60% |
| Minimum compression ratio | 30% reduction |
| Maximum source size (images) | 200MB |
| Maximum source size (videos) | 500MB |
| Minimum video bitrate | 500 kbps |
| Platform | Limit |
|---|---|
| Telegram (Premium) | 2GB |
| Telegram (free) | 500MB |
| Discord (free) | 25MB |
| Discord (Nitro) | 500MB |
| Default config | 10MB |
Files larger than the configured limit are compressed if possible, otherwise the text message is still forwarded.
The Telegram session file is stored in ./sessions/ directory:
- Session name:
media_forwarder.session(configurable viaTELEGRAM_SESSION_NAME) - Location: Mounted as Docker volume
- Persistence: Survives container restarts
- Security: Contains authentication tokens, keep it private
To re-login, delete the session file and run the login command again:
rm ./sessions/media_forwarder.session
docker compose run --rm media-forwarder loginError: session not authorized; run the 'login' command first
Solution: Run the login command to create a new session
docker compose run --rm media-forwarder loginError: Channel not found or No configuration found for channel
Solution:
- Verify the channel username or ID in
config/channels.yaml - Make sure your Telegram account has access to the channel
- Try using the numeric ID instead of username
Error: Failed to send to Discord
Solution:
- Verify the webhook URL is correct
- Check Discord server status
- Ensure the webhook has permissions to post in the channel
- Check logs for detailed error messages
Warning: Media too large, attempting compression
Solution:
- Increase
max_file_size_mbin configuration - Note: Discord has hard limits (25MB free, 500MB Nitro)
go build -o forwarder ./cmd/forwarder/# Set environment variables
export TELEGRAM_API_ID=your_api_id
export TELEGRAM_API_HASH=your_api_hash
# Validate config
./forwarder validate --config config/channels.yaml
# Login
./forwarder login
# Run
./forwarder run --config config/channels.yamlgo test ./... -v -coverRun tests with coverage report:
go test ./... -v -cover -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.htmldocker build -t media-forwarder .docker run --rm \
-v $(pwd)/sessions:/app/sessions \
-v $(pwd)/config:/app/config \
--env-file .env \
media-forwarderThis project uses GitHub Actions for:
- Automated Testing: Runs Go tests with race detection on every push and pull request
- Coverage Gate: Enforces ≥85% coverage on core packages (config, discord)
- Docker Image Building: Builds and pushes Docker images to GitHub Container Registry
- Coverage Reporting: Uploads coverage reports to Codecov
The Docker image is available at: ghcr.io/haswelldev/media-forwarder:main
| Component | Technology |
|---|---|
| Language | Go |
| Telegram Client | gotd/td (MTProto library for user accounts) |
| Discord Client | net/http (webhook-based, with retry and rate-limit handling) |
| Configuration | YAML with Go struct validation |
| Image Compression | Go image/jpeg stdlib |
| Video Compression | ffmpeg (subprocess) |
| Container | Multi-stage Docker build (Go → Debian slim + ffmpeg) |
| Testing | Go testing package with httptest mocks |
This is a complete rewrite from Python to Go. Key differences:
- Config format: 100% backward compatible — same
channels.yamland.envfiles - Session files: Not compatible — you must re-login (
docker compose run --rm media-forwarder login) - Translation feature: The
translate_captionsconfig field is accepted but has no effect - Binary: Single static binary, no Python runtime needed
- Docker image: Smaller (~120MB vs ~180MB) and starts faster (~50ms vs ~2-5s)
- Session files contain authentication tokens — keep them private
- API credentials should be stored in environment variables
- Webhook URLs should be kept secret
- Never commit
.envfiles or session files to version control
MIT
For issues and questions, please open an issue on GitHub.