Testgram is a fork of MyTelegram — a self-hosted C# implementation of the Telegram server-side API.
- API Layer:
224 - MTProto Transports:
Abridged,Intermediate - Private Chat
- Supergroup Chat
- Channel
- Message Reactions
- Star Gifts (channels, hide/show, unread mentions)
- Passkey Login (WebAuthn)
- Channel Direct Messages (Monoforum)
- Bot Support
- Stories
- Privacy Settings & 2FA
- Voice & Video Calls (1:1, WebRTC)
- Group Calls / Voice & Video Chats
- Conference Calls (E2E encrypted)
- Live Streams (RTMP / HLS)
- Push Notifications (APNS / FCM / WebPush)
- Email Sender & 2FA Recovery Email
- Telegram Business
- Auto-Delete Messages
- Stickers
- Scheduled Messages
- Forum Topics
- Themes & Wallpapers
- Folders (Dialog Filters)
- End-to-End Encrypted Chat (Secret Chats)
- Full Email Login
- Download the Docker Compose files:
curl -O https://raw.githubusercontent.com/glebxdlolreal/testgram/dev/docker/compose/docker-compose.yml
curl -O https://raw.githubusercontent.com/glebxdlolreal/testgram/dev/docker/compose/.env.example
cp .env.example .env
-
Edit
.env:- Replace
YOUR_SERVER_IPwith your server's public IP address - Set strong passwords for
CHANGE_MEfields (RabbitMQ, Minio, encryption keys)
- Replace
-
Start the server:
mkdir -p ./data/mytelegram
chmod -R a+w ./data/mytelegram
docker compose up -dKey .env settings:
| Variable | Description |
|---|---|
App__DcOptions__0__IpAddress |
Your server's public IP |
RabbitMQ__Connections__Default__Password |
RabbitMQ password |
App__AccessHashSecretKey |
Random secret key |
App__EncryptionConfig__MessageKeys__0__Key |
Base64 encryption key |
App__FixedVerifyCode |
Fixed SMS code for testing (leave empty in production) |
Calls require a TURN/STUN server. As of the latest version this is bundled:
the docker compose stack ships a coturn service (STUN/TURN) and a rtmp-server
service (mediamtx, used for group-call live streams). You no longer need to install
Coturn on the host — just point the WebRTC config at your server IP.
Configure WebRTC in .env (the credentials must match the bundled coturn user,
which defaults to testgram:testgram2024):
# REQUIRED for calls to work
App__WebRtcConnections__0__Ip=YOUR_SERVER_IP
App__WebRtcConnections__0__Port=3478
App__WebRtcConnections__0__Turn=True
App__WebRtcConnections__0__Stun=True
App__WebRtcConnections__0__UserName=testgram
App__WebRtcConnections__0__Password=testgram2024Group calls and live streams use the bundled RTMP/HLS server:
App__RtmpStreamUrl=rtmp://YOUR_SERVER_IP:1935/live
App__RtmpHlsUrl=http://rtmp-server:8888/live
RTMP_PORT=1935
RTMP_HLS_PORT=8888MongoDB call indexes are created automatically on first start via the call-init
container. To run them manually:
cd scripts && ./setup_call_indexes.sh # Optional: manual setupOpen the required UDP/TCP ports on your firewall: 3478 (STUN/TURN), 49152-49172/udp
(TURN relay), and 1935 (RTMP). See docs/CALLS_SETUP.md for
complete setup instructions, including using an external TURN server.
If clients fail to connect with an error like:
Attempt 1 at connecting failed: ConnectionRefusedError: [WinError 1225] The remote computer refused the network connection
but the VDS/host itself is reachable, the gateway is most likely not listening on the
main port 20443 (DC1, the first port clients connect to — see App__DcOptions__0__Port).
Cause: App__Servers__0__Enabled is unset/commented in .env. docker-compose always
passes this variable to the gateway container, so an unset value becomes an empty
string. An empty value makes .NET drop server 0 from the config entirely, so the
gateway never opens the 20443 listener and every connection is refused.
Fix: make sure .env contains an active line (not commented, not empty):
App__Servers__0__Enabled=TrueThen recreate the gateway and verify it listens on 20443:
cd docker/compose
docker compose up -d --force-recreate gateway-server
docker compose logs gateway-server | grep 20443 # expect: "Tcp server started at ...:20443"If file-server logs are spammed with:
Minio.Exceptions.InvalidBucketNameException: MinIO API responded with message=Bucket name cannot be empty.
and avatars, stickers, or custom verification icons fail to load in clients, Minio__BucketName
is unset/commented in .env. docker-compose always passes this variable to file-server, so an
unset value becomes an empty string, and every file request fails.
Fix: make sure .env contains active lines (not commented, not empty):
Minio__BucketName=tg-files
Minio__CreateBucketIfNotExists=TrueThen recreate file-server:
cd docker/compose
docker compose up -d --force-recreate file-serverIf file-server logs are flooded with:
[ERR] Get file failed, input: FileId: "..." Offset: ... Limit: 32768
System.NullReferenceException: Object reference not set to an instance of an object.
at Minio.MinioClient.ParseWellKnownErrorNoContent(ResponseResult response)
...
at MyTelegram.FileServer.Services.MinioStoringHelper.GetAsync(...)
this is a regression in the MinIO .NET SDK bundled inside the upstream
mytelegram-file-server image (Minio 6.0.6-local). When MinIO answers a byte-range
request with 416 Range Not Satisfiable (no body) — which Telegram clients trigger
for the final chunk of a download (offset at/after EOF) — the SDK fails to handle the
416, leaves its error object null, and throw error; turns into a NullReferenceException.
Because the file-server is built and published separately, it can't be patched from this repo. Instead, file-server is routed through the minio-proxy service (a tiny nginx proxy) which rewrites those 416 responses into a clean empty 200 the SDK accepts. All other traffic passes through untouched.
This is wired up by default (Minio__FileServerEndpoint defaults to minio-proxy:9000).
If you see this error, make sure the proxy is running and file-server points at it:
cd docker/compose
docker compose up -d minio-proxy
docker compose up -d --force-recreate file-server# Linux amd64
cd build/docker && ./build-all-amd64.sh
# Linux arm64
cd build/docker && ./build-all-arm64.sh| Platform | Repository |
|---|---|
| Android | https://github.com/glebxdlolreal/testgram-android |
| Desktop (TDesktop) | https://github.com/glebxdlolreal/testgram-tdesktop |
| iOS | https://github.com/loyldg/mytelegram-iOS |
| WebK | https://github.com/loyldg/mytelegram-webk |
| WebA | https://github.com/loyldg/mytelegram-weba |
- Clone the client source code.
- Search for
YOUR_SERVER_IPin all files and replace it with your own server IP.
The repo includes a Telegram bot (bot/) that listens for registration codes via RabbitMQ and sends them to users via Telegram.
cd bot
cp .env.example .env
# Edit .env with your BOT_TOKEN and RABBITMQ_URL
python3 bot.pyConnect to MongoDB and run:
// mongosh tg
// 1. Add balance
db['star-transactions'].insertOne({
UserId: Long('USER_ID'),
Amount: 1000, // number of stars
Gift: false,
Title: 'Admin top-up',
PeerUserId: 0,
Date: new Date()
});
db['eventflow-userreadmodel'].updateOne(
{ UserId: Long('USER_ID') },
{ $inc: { StarsBalance: 1000 } }
);Replace
USER_IDwith the target user ID (find it viadb['eventflow-userreadmodel'].find({UserName: 'username'})).
Gifts are stored in the star-gifts collection. To add a new gift:
// mongosh tg
db['star-gifts'].insertOne({
GiftId: Long('UNIQUE_GIFT_ID'), // unique ID (e.g. 1001)
Stars: 50, // price in stars
Title: 'My Gift',
Description: '',
DocumentId: Long('DOCUMENT_ID'), // sticker/document ID from Telegram
LimitedQuantity: 0, // 0 = unlimited
SoldCount: 0,
Available: true,
FirstSaleDate: new Date(),
LastSaleDate: null
});To give a gift to a user directly (without purchase):
db['saved-star-gifts'].insertOne({
UserId: Long('RECIPIENT_USER_ID'),
FromUserId: Long('0'),
GiftId: Long('UNIQUE_GIFT_ID'),
Stars: 50,
Message: '',
Saved: true,
Date: new Date()
});To make a gift upgradeable, you need to:
1. Set upgrade cost on the gift:
// mongosh tg
db['star-gifts'].updateOne(
{ GiftId: Long('GIFT_ID') },
{ $set: {
UpgradeStars: 1000, // stars required to upgrade
AvailabilityTotal: 10000 // total unique copies that can exist
}}
);2. Add upgrade config (attributes for unique version):
Each unique gift gets 3 attributes: model (sticker), backdrop (background), pattern (overlay).
Add variants to star-gift-upgrade-config:
db['star-gift-upgrade-config'].insertMany([
// Model (sticker variant)
{
gift_id: Long('GIFT_ID'), // 0 = applies to all gifts
type: 'model',
name: 'Rare Model',
rarity_permille: 100, // 100 = 10% chance (out of 1000)
document_id: Long('STICKER_DOCUMENT_ID')
},
// Backdrop (background colors)
{
gift_id: Long('GIFT_ID'),
type: 'backdrop',
name: 'Golden',
rarity_permille: 50,
backdrop_id: 1,
center_color: 0xF1C40F,
edge_color: 0xD4AC0D,
pattern_color: 0xF9E79F,
text_color: 0xFFFFFF
},
// Pattern (overlay sticker)
{
gift_id: Long('GIFT_ID'),
type: 'pattern',
name: 'Stars',
rarity_permille: 200,
document_id: Long('PATTERN_DOCUMENT_ID')
}
]);
rarity_permille— weight out of 1000 (higher = more common). Usegift_id: 0for attributes shared across all gifts.
3. Force-upgrade a gift for a user (admin):
// Find the saved gift
db['saved-star-gifts'].findOne({ OwnerUserId: Long('USER_ID'), IsUnique: false });
// Then trigger upgrade via API or set UpgradeStars: 0 to make it free
db['star-gifts'].updateOne(
{ GiftId: Long('GIFT_ID') },
{ $set: { UpgradeStars: 0 } }
);After deploying the server, run the reaction seeder to populate emoji reaction animations:
cd scripts
# 1. Download reaction files from Telegram (~50MB)
TG_API_ID=your_api_id \
TG_API_HASH=your_api_hash \
TG_PHONE=+1234567890 \
python3 seed_reactions.py --download
# 2. Import files into Minio + MongoDB
MONGO_URL=mongodb://localhost:27017 \
MINIO_ENDPOINT=localhost:9000 \
MINIO_ACCESS_KEY=your_key \
MINIO_SECRET_KEY=your_secret \
python3 seed_reactions.py --import
# 3. Generate the C# handler with real document IDs
MONGO_URL=mongodb://localhost:27017 \
HANDLER_PATH=../source/src/MyTelegram.Messenger/Handlers/LatestLayer/Messages/GetAvailableReactionsHandler.cs \
python3 seed_reactions.py --generate-handler
# 4. Rebuild and redeploy messenger images
cd ../build/docker
export REGISTRY_URL="mytelegram"
bash 1.build-messenger-command-server.sh
bash 2.build-messenger-query-server.sh
cd ../../docker/compose && docker compose down && docker compose up -dNote: Steps 1–3 only need to be done once. The generated handler is committed to the repo so subsequent deploys don't require re-seeding.