A private Telegram userbot that downloads media links with yt-dlp or
gallery-dl and uploads the resulting images and videos back to Telegram.
- Rust toolchain
yt-dlpgallery-dlffmpegandffprobe
The included Nix flake provides these dependencies:
nix develop
cargo runThe flake provides yt-dlp 2026.10.26 or newer, overriding older nixpkgs versions
while allowing future nixpkgs updates to supply a newer release. Both the
development shell and deployed service use the same version.
Grammers is pinned to an upstream 0.11.0 revision containing the October 2026
connection buffer fixes. Refresh Cargo.lock with cargo update when updating
dependencies; changing the Grammers revision also requires updating its source
hash in flake.nix.
Telegram API credentials come from environment variables; everything else lives
in a JSON config file. Copy config.example.json and fill in your user IDs:
cp config.example.json config.json| Variable | Required | Default | Description |
|---|---|---|---|
API_ID |
Yes | — | Telegram API ID |
API_HASH |
Yes | — | Telegram API hash |
CONFIG_FILE |
No | config.json |
Path to the JSON config file |
superusers and allowed_users are Telegram user ID lists. Superusers are
implicitly authorized and can run administrative commands. Every other key is
optional and defaults to the value shown in config.example.json; all numeric
settings must be positive integers. /add and /remove rewrite the
allowed_users array in place, so edit the file by hand only while the bot is
stopped.
yt_dlp_domains is an optional list of hostnames that forces video downloads
through yt-dlp, for example "yt_dlp_domains": ["media.example.com"]. Each entry
matches that hostname and its subdomains, ignoring case; schemes, paths, ports,
and wildcards are rejected. Matching uses the URL's hostname, so names appearing
in paths or query strings do not trigger an override. The default empty list
keeps the existing routing rules. Audio and clip requests always use yt-dlp.
Restart the bot after changing this setting. Keep private domain overrides in
your ignored config.json or the file selected by CONFIG_FILE.
telegram_use_ipv6 defaults to false. Set it to true on a host with working
IPv6 when its IPv4 route to Telegram is slow or frequently disconnects. Restart
the service after changing it. The selected address family is logged at startup.
On Linux with systemd, deploy/endgame.service runs the bot as the dedicated
endgame user. Its working directory is /var/lib/endgame, which holds the
editable config.json, Telegram session and cookie files. The service reads
API_ID and API_HASH from /etc/endgame/environment; keep this file owned
by root with mode 0600 and do not put credentials in the Nix store or Git.
The bot also keeps a private downloads.sqlite database in this directory. Keep
it and its SQLite sidecar files with the Telegram session when moving or backing
up an installation; it records pending work and which requests were finished.
For a first installation, run these commands as root from the Git checkout:
useradd --system --user-group --no-create-home \
--home-dir /var/lib/endgame --shell /usr/sbin/nologin endgame
install -d -m 0700 -o endgame -g endgame /var/lib/endgame
install -d -m 0700 -o root -g root /etc/endgame
install -d -m 0755 /opt/endgame
nix build --extra-experimental-features 'nix-command flakes' \
--out-link /opt/endgame/current .#endgame
install -m 0644 deploy/endgame.service /etc/systemd/system/endgame.serviceCreate /etc/endgame/environment with mode 0600, containing API_ID=... and
API_HASH=... on separate lines. Stop any existing bot instance before copying
config.json, userbot.session and its sidecar files, and any cookie files to
/var/lib/endgame. Give those files to endgame:endgame with mode 0600.
Copy the session and its sidecars together while the old process is stopped;
otherwise its latest login state can be lost. Keep the original files as a
backup until the service has started successfully.
systemctl daemon-reload
systemctl enable --now endgame.service
systemctl status endgame.service
journalctl -u endgame.service -fThe service expects an already authorized Telegram session because it has no interactive terminal. If login is needed, stop the service and run the same binary interactively under the service account using the service environment:
systemctl stop endgame.service
systemd-run --wait --pty --collect --property=User=endgame \
--property=Group=endgame --property=WorkingDirectory=/var/lib/endgame \
--property=EnvironmentFile=/etc/endgame/environment \
/opt/endgame/current/bin/endgameAfter signing in, stop the interactive bot with Ctrl-C and start the service. The service sends the same signal on shutdown and allows up to two minutes for accepted jobs to drain; longer work may be interrupted when that timeout ends.
To deploy an update, run the following as root from the checkout. The recipe
pulls the current branch, rebuilds the rooted package, installs the unit, and
restarts the service. Enter nix develop first if just is not on your PATH.
just update- Send one or more URLs in a private message. Every URL becomes a separate, numbered job.
- Links and
/audio,/video,/best, and/cliprequests sent while the bot is offline are picked up automatically when it starts again. Pending and interrupted jobs survive restarts, and completed requests are not replayed. Identical links sent in separate messages still create separate jobs. - Multi-item gallery downloads are returned as Telegram albums in batches of at most ten items.
- Use
/cancel <job-id>to cancel one of your queued or active jobs, or reply to its status message with/cancelso no job ID is needed. - Reply to any download status with
/retryto enqueue the same download again. This also works after restarting the bot and regardless of whether the original job failed. - Use
/audio <url>for an MP3 extraction. - Use
/video 720p <url>to cap video quality, or/best <url>for the unrestricted default. - Use
/clip 1:30 1:45 <url>to download an accurately cut section. Timestamps accept seconds,MM:SS, orHH:MM:SS, with optional millisecond precision. - Use
/statusor/queuefor current queue usage,/pingto check whether the userbot is alive, and/helpfor the command summary (/hremains an alias for/ping). - Superusers are implicitly authorized and can use
/add <user-id>to authorize another user or/remove <user-id>to revoke access. Both commands update theallowed_usersarray in the config file, so changes take effect immediately and survive restarts. Superusers can only be changed by editing the config file./userslists everyone allowed to download, including Telegram usernames and full names when available. - Superusers can use
/insta <cookie-content>to replace Instagram cookies. The cookie message is deleted and the local file is written with owner-only permissions on Unix systems.
If Instagram rejects a post with an authentication error, the bot retries it
without saved cookies using Instagram's public page. This fallback supports
photos, videos, and mixed carousels, preserving carousel order and album delivery.
Private or restricted posts still need working account cookies. If Instagram
requires an account checkpoint, open Instagram in your browser, complete the
verification, and export fresh cookies with /insta. A generic 400 Bad Request
from the media-info API can indicate this checkpoint even before the cookies expire.
Each accepted job maintains one status message as it moves through queued, downloading, inspection, transcoding, thumbnail preparation, uploading, finalization, and a terminal state. yt-dlp downloads and transcodes report progress in five-percent steps. Cancellation is owner-scoped: users cannot cancel or inspect another user's jobs. While primary media is uploading, the status includes the current item's percentage and estimated bytes remaining. Progress edits are throttled to avoid excessive Telegram requests; thumbnail uploads are not included.
Requests are saved before Telegram's update checkpoint advances. A large offline
backlog waits on disk and feeds into the normal bounded queue, keeping the same
download concurrency. /status includes work waiting on disk, and /cancel
can cancel it before it starts. Failed or cancelled jobs are not automatically
retried; use /retry or send the link in a new message. During recovery, an
existing status that already contains delivered media is recognized and kept
instead of uploading that result again.
Status edits and other Telegram delivery operations have bounded waits, while
failed status edits are retried without blocking the media worker. /status
shows each caller's active job phase and how long it has been in that phase.
For a single image, video, or audio result, that status message is edited into the final media message and its progress caption is explicitly cleared. For galleries, the album is sent and the temporary status is deleted. Failures and cancellations remain visible by editing the same status message.
Photos in formats Telegram cannot send as photos, including WebP, are converted to JPEG before upload. This applies to both single photos and album items; compatible JPEG and PNG files keep their original bytes.
Videos that are not already MP4/H.264, or that exceed MAX_UPLOAD_SIZE_MB, are
converted to MP4/H.264/AAC with a streaming-friendly layout. Temporary media,
thumbnails, and cancelled downloads are removed after processing.
For YouTube, the downloader first prefers an H.264 MP4 format estimated to fit
under the upload ceiling, which avoids needless multi-hour transcodes when a
slightly lower compatible format is available. Playlist expansion is disabled;
each submitted URL remains one job.
Other video sources also prefer H.264 MP4 when available, avoiding unnecessary
transcoding of codecs such as VP9 (including its vp09 format label).
External commands, uploads, and whole jobs are watched by configurable deadlines. Logs include job phase transitions plus command PID and elapsed time, and a panicking worker is converted into a visible failed job.
Uploads restart from byte zero after temporary Telegram connection failures or server errors, with at most three attempts and a single overall upload deadline. Cancellation interrupts both the upload and retry delays. Optional thumbnail uploads have a separate 15-second limit so they cannot hold up an otherwise ready video for minutes. Completed uploads log their byte count and elapsed time. yt-dlp uses a 30-second socket timeout, bounded retries, and four concurrent fragments for supported DASH/HLS downloads. Host-level yt-dlp configuration files are ignored, and yt-dlp chooses its extractor-specific user agent.
When diagnosing a slow job, compare the downloader's ended after log with the
Telegram upload's start and completion times. request error: read 0 bytes means
Telegram's connection closed; it does not mean yt-dlp downloaded an empty file.
Set RUST_LOG in the service environment to adjust logging, for example
RUST_LOG=info,grammers_mtsender=debug for connection diagnostics.
For manual Telegram checks using a copy of the bot's session, stop the service first, finish and close the diagnostic client, then restart the service last. Telegram delivers updates to the last active connection for an authorization key. A diagnostic client can take over live updates even while the service stays connected; restarting the service restores its subscription.
On Ctrl-C, new work stops and jobs already in memory are drained before the Telegram connection closes. Other pending jobs remain on disk for the next start. A forced shutdown also leaves interrupted work available for recovery.
If Telegram returns a temporary server error while initializing a connection
(for example, rpc error -500: No workers running caused by invokeWithLayer),
the bot retries up to eight connection attempts with increasing delays, capped
at 30 seconds (two minutes of delays in total). This error means Telegram's
servers are temporarily unavailable; keep userbot.session and try again later
if the retries are exhausted. Grammers handles salt resets automatically.
cargo test
cargo clippy --all-targets -- -D warningsmain.rsis the composition root and live-update dispatcher.commands.rs,cookies.rs, andtelegram/auth.rsisolate Telegram-facing input concerns.jobs/queue.rsschedules work,jobs/progress.rscoordinates progress state, andjobs/status.rsrenders user-facing status text.jobs/inbox.rspersists requests and completion records, andtelegram/checkpoint.rskeeps offline update batches recoverable until they have been recorded.media/pipeline.rsorchestrates jobs. Subprocesses, downloader policy, delivery, inspection, progress parsing, transcoding, request types, and temporary workspaces live in separate modules undermedia/.
Every job downloads and generates derived media inside a unique temporary workspace. The workspace is explicitly removed asynchronously when the job finishes.