Add the dependency

In your project’s ziran.toml; pin ref to a commit for reproducible builds.

[dependencies.telegram]
git = "https://github.com/ziranlang/telegram.git"
ref = "master"

Overview

Exports are package-qualified: telegram/tl, telegram/mtproto, telegram/client, telegram/auth, telegram/messages, telegram/members and telegram/session. Use ziran add https://github.com/ziranlang/telegram.git once this repository is published. For development, resolve the canonical checkout through an ignored ziran.local.toml override and commit the generated lock file.

client owns an authenticated encrypted connection and accepts TL objects as JSON. Bytes use base64; int128 and int256 use hexadecimal. Integer fields are serialized without converting through floating point. auth adds phone number, code and SRP two-step verification, private session snapshots and authorization transfer between data centers. Account applications need their own API ID and hash from Telegram's app configuration.

session provides a single-account, single-thread event interface for CLI and native hosts: Create, Send, Receive, Execute. Its high-level JSON request names preserve compatibility with existing mirror callers. Account history, bot membership, text sends, exact callback bytes, reactions, read receipts, edits, deletions and service messages are normalized in Ziran. The same connection receives updates and recovers account and channel sequence gaps. It does not start a bot-token poller.

Before setTdlibParameters, create an owner-controlled directory with mode 0700 and provide its path as database_directory. That historical request name initializes this package, without TDLib. The session pins the directory and stores account.json with mode 0600 using fsync and an atomic rename. Existing unsafe or corrupt account files are rejected and preserved. Codes and passwords are not included in account snapshots. Drain and apply received events before calling Receive again: update cursors are checkpointed after the queue is drained so interrupted processing can replay safely.

The high-level session covers the operations used by account chat mirrors; it is not a complete Telegram application. loadChats announces every chat with its full name, newest message and mute state. downloadMessageFile saves a message's photo or document to a new owner-only file, connecting to the file's data center when it differs from the account's; downloadChatPhoto does the same for a chat's small profile photo. sendMessage sends text, or uploads a local photo or document (inputMessagePhoto / inputMessageDocument with a path), optionally as a reply. forwardMessages forwards messages between chats and searchChats finds people, bots, groups and channels by name, @username or +phone. getChat reports a group's member count, description and the basic group it was upgraded from. searchChatMembers lists a group's recent members (up to 200) with their roles, custom titles and users; getUser gives one user's presence, bot message access, active username and profile photo; getChatMessageCount counts a chat's photos, videos, files, audio, links, voice messages or GIFs with TDLib's search filters. Normalized messages carry reply_to_message_id and forward_info. Other API methods can be called through client and the bundled TL schema. Secret chats, account registration and interactive email enrollment do not have high-level session helpers.

Run make check test for offline TL, SRP, normalization and update-sequence checks. make rpc-test is an explicit network test: it performs an encrypted help.getConfig request against Telegram's test DC without signing into an account or sending a message.

Protocol references: authorization, update sequences, SRP, and MTProto 2.0.

telegram/src/tl.zi

tl

Bounded, schema-driven TL binary serialization. It preserves signed 64-bit integers and optional fields without a JavaScript/JSON-number round trip.

Import with #import "telegram/tl".

Definition :: struct { name: string; id: u32; fields: string; result: string; }
definitions: [4096]Definition
definition_count: s32
initialized: bool
Buffer :: struct { bytes: []u8; at: s32; valid: bool; size: s32; }
failure: string

Why the last Decode or DecodeResponse failed, for error messages.

Fail :: (buffer: *Buffer, reason: string)
Start :: (bytes: []u8) -> Buffer
Join :: (a: string, b: string) -> string
Find :: (text: string, needle: u8, start: s32) -> s32
Init :: () -> bool
ByName :: (name: string) -> s32
ById :: (id: u32) -> s32
Byte :: (buffer: *Buffer, value: u8)
ReadByte :: (buffer: *Buffer) -> u8
Word :: (buffer: *Buffer, value: u32)
ReadWord :: (buffer: *Buffer) -> u32
Long :: (buffer: *Buffer, value: s64)
ReadLong :: (buffer: *Buffer) -> s64
Bytes :: (buffer: *Buffer, value: []u8)
ReadBytes :: (buffer: *Buffer) -> s32
Field :: struct { name: string; type: string; flag: s32; bit: s32; }
ParseField :: (token: string) -> Field
Next :: (text: string, at: *s32) -> string
EncodeText :: (buffer: *Buffer, kind: string, json: string, at: s32)
EncodeType :: (buffer: *Buffer, kind: string, json: string, at: s32, depth: s32)
DecodeType :: (buffer: *Buffer, kind: string, out: *Vec(u8), depth: s32)
Double :: (buffer: *Buffer, out: *Vec(u8))

A double (video durations, coordinates, ratings) is written with up to nine decimals. JSON has no NaN or infinity, and values too large to round exactly that way are not real Telegram values; both become null.

Encode :: (json: string, bytes: []u8) -> s32
Decode :: (bytes: []u8) -> string
Finished :: (buffer: *Buffer, kind: string) -> bool

A failed or partly read reply records why in failure.

DecodeResponse :: (method: string, bytes: []u8) -> string

Source ↗

telegram/src/encoding.zi

encoding

Import with #import "telegram/encoding".

Alphabet :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
HexAlphabet :: "0123456789abcdef"
Hex :: (bytes: []u8) -> string
Nibble :: (b: u8) -> s32
Unhex :: (text: string, bytes: []u8) -> bool
Base64 :: (bytes: []u8, url: bool) -> string
Digit :: (b: u8) -> s32
Unbase64 :: (text: string, bytes: []u8) -> s32
Quote :: (text: string) -> string
String :: (json: string, at: s32, name: string) -> string
Integer :: (text: string) -> s64
Raw :: (json: string, at: s32) -> string
Number :: (json: string, at: s32, name: string) -> s64
Kind :: (json: string) -> string
Copy :: (text: string) -> string
Trim :: (text: string) -> string

Source ↗

telegram/src/mtproto.zi

mtproto

MTProto 2.0 intermediate TCP transport and authenticated key exchange. All protocol and cryptography are Ziran; sockets and entropy are OS services.

Import with #import "telegram/mtproto".

Connection :: struct { socket: Socket.LinuxSocket; key: [256]u8; key_id: s64; salt: s64; session: s64; sequence: s32; last_id: s64; clock: s64; dc: s32; test: bool; ready: bool; error: string; received: [128]s64; receive_index: s32; }
Builder :: () -> Vec(u8)
Now :: () -> s64
Integer :: (bytes: []u8) -> s64
NewId :: (connection: *Connection) -> s64
Address :: (dc: s32, test: bool) -> [4]u8
Open :: (connection: *Connection, dc: s32, test: bool) -> bool
SendPacket :: (connection: *Connection, bytes: []u8) -> bool
Exact :: (connection: *Connection, bytes: []u8, timeout: s32) -> bool
ReceivePacket :: (connection: *Connection, bytes: []u8, timeout: s32) -> s32
Unencrypted :: (connection: *Connection, json: string) -> string
AddMod :: (a: u64, b: u64, n: u64) -> u64
MultiplyMod :: (a: u64, b: u64, n: u64) -> u64
Gcd :: (a: u64, b: u64) -> u64
Factor :: (n: u64) -> u64
Natural :: (value: u64) -> string
RsaPad :: (data: []u8, test: bool, output: []u8) -> bool
Prime :: "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b"
ValidDh :: (prime: []u8, g: s32, public: []u8) -> bool
KeyExchange :: (connection: *Connection) -> bool
Derive :: (connection: *Connection, msg_key: []u8, incoming: bool, key: []u8, iv: []u8)
Encrypt :: (connection: *Connection, body: []u8, content: bool, output: []u8) -> s32
Decrypt :: (connection: *Connection, input: []u8, output: []u8) -> s32

Source ↗

telegram/src/client.zi

client

A synchronous RPC client with a separately drainable update queue. The caller owns one Client and calls it from one thread. API methods are TL objects; bytes fields use base64 and int128/int256 fields use hex.

Import with #import "telegram/client".

Client :: struct { connection: MT.Connection; api_id: s32; api_hash: string; device: string; initialized: bool; response: string; pending: s64; method: string; retry: bool; updates: [256]string; head: s32; tail: s32; gap: bool; }
Error :: (code: s32, message: string) -> string
Connect :: (client: *Client, dc: s32, test: bool) -> bool
Queue :: (client: *Client, value: string)
NextUpdate :: (client: *Client) -> string
Send :: (client: *Client, json: string, content: bool) -> s64
Ack :: (client: *Client, id: s64)
Unpack :: (body: []u8, output: []u8) -> s32
Handle :: (client: *Client, body: []u8, server_id: s64, depth: s32)
Poll :: (client: *Client, timeout_ms: s32) -> bool
Request :: (client: *Client, query: string) -> string

Source ↗

telegram/src/auth.zi

auth

Account authorization over the Ziran MTProto client. The caller stores Snapshot in private storage; phone codes and passwords are never persisted.

Import with #import "telegram/auth".

Account :: struct { client: RPC.Client; phone: string; code_hash: string; state: string; user: string; error: string; password_parameters: string; qr: string; qr_expires: s64; }
HashBytes :: (bytes: []u8, output: []u8)
Salted :: (data: []u8, salt: []u8, output: []u8)
PasswordProof :: (parameters: string, password: string, entropy: []u8) -> string
Snapshot :: (account: *Account) -> string
Restore :: (account: *Account, snapshot: string) -> bool
ResumePassword :: (account: *Account, parameters: string) -> bool

Recover a pending two-factor challenge using the saved authorization key. No phone code or password is stored in the snapshot.

Connect :: (account: *Account, api_id: s32, api_hash: string, device: string, snapshot: string) -> bool
Request :: (account: *Account, query: string) -> string
Advance :: (account: *Account, reply: string) -> bool
Phone :: (account: *Account, phone: string) -> bool
Code :: (account: *Account, code: string) -> bool
Password :: (account: *Account, password: string) -> bool

Source ↗

telegram/src/messages.zi

messages

Normalize Telegram API messages without losing callback bytes or peer IDs.

Import with #import "telegram/messages".

ChatId :: (peer: string) -> s64
Peer :: (chat: s64) -> string
Normalize :: (message: string, chat: s64, self: s64) -> string

Source ↗

telegram/src/members.zi

members

Normalize chat members, user presence and shared-media counts into the TDLib-style objects the session returns.

Import with #import "telegram/members".

Flag :: (json: string, at: s32, name: string) -> bool
Bool :: (value: bool) -> string
User :: (json: string, at: s32) -> string

A user's presence, bot access and active username. Photo IDs are 64-bit, so they travel as text like chat photo IDs do.

Members :: (reply: string, limit: s64) -> string

Members from channels.channelParticipants or messages.chatFull, with each member's user alongside so callers need no lookup per member.

Filter :: (name: string) -> string

TDLib message filters and the API filters that count the same messages.

CountQuery :: (peer: string, filter: string) -> string
Count :: (reply: string) -> string

One search counter, from the vector messages.getSearchCounters returns.

Source ↗

telegram/src/session.zi

session

One account connection, sign-in state and event queue for CLI/native clients. This small JSON interface is implemented entirely over Ziran MTProto/TL.

Import with #import "telegram/session".

Join :: (a: string, b: string) -> string
account: Auth.Account
directory: string
directory_fd: s32
last_snapshot: string
queue: [8192]string
head: s32
overflow: bool
tail: s32
peer_count: s32
peer_hashes: [4096]s64
peer_ids: [4096]s64
channel_gap: [4096]bool
channel_pts: [4096]s64
channel_saved: [4096]s64
offset_date: [2]s64
offset_id: [2]s64
offset_peer: [2]string
date: s64
next_difference: s64
next_sync: s64
pts: s64
qts: s64
seq: s64
saved_date: s64
saved_pts: s64
saved_qts: s64
saved_seq: s64
recovering: bool
started: bool
send_success: string
Push :: (value: string)
Authorization :: ()
Create :: () -> s32
Private :: (fd: s32, kind: u32) -> bool
Load :: () -> string
RestoreUpdates :: (snapshot: string)
Checkpoint :: ()
Snapshot :: () -> string
Save :: () -> bool
Reply :: (value: string, extra: string)
Error :: (text: string, extra: string, code: s64)

Telegram's own error code is kept (404 ends loadChats, 420 is a flood wait); local and transport failures report 400.

Cache :: (reply: string)
InputPeer :: (id: s64) -> string
Request :: (query: string) -> string
PeerSlot :: (id: s64) -> s32
Message :: (message: string, chat: s64)
Advance :: (update: string, chat: s64) -> bool
Update :: (update: string)
State :: (value: string)
Difference :: () -> bool
ChannelDifference :: (slot: s32) -> bool
Announce :: (reply: string, folder: s32) -> s32

Announces each dialog in a messages.getDialogs reply as a chat: its title (a person's full name), type, newest message and mute state.

Dialogs :: (folder: s32) -> string
media: [6]RPC.Client

Media lives on its own data center; each gets one connection authorized with an authorization exported from the account's data center.

media_ready: [6]bool
Release :: (value: string)

Large replies are released at once instead of living until exit.

FileRequest :: (dc: s32, query: string) -> string
PhotoSize :: (message: string, photo: s32) -> string

The largest photo size, as (thumb type, bytes).

Download :: (request: string) -> string

Downloads a message's photo or document to a new private file at destination. Files larger than limit bytes are refused before download.

Fetch :: (place: string, dc: s32, destination: string, mime: string, name: string) -> string

Saves the file at location on data center dc to a new owner-only file.

ChatPhoto :: (request: string) -> string

A chat's small profile photo (160 by 160 JPEG) as a new owner-only file.

upload_error: string

Uploads a local file in 512 KiB parts and returns its InputFile.

Upload :: (path: string, name: string) -> string
ReplyTo :: (request: string) -> string
SentId :: (reply: string) -> s64

The new message's ID from a send reply; other updates apply as usual.

Pending :: (chat: s64, id: s64) -> string
Media :: (content: string, file: string, name: string) -> string

Uploaded voice notes carry Telegram's audio/voice attribute instead of arriving as a downloadable document. Photo and document behavior stays.

SendMedia :: (request: string, chat: s64, peer: string) -> string

A photo or document from a local path, with an optional caption.

Forward :: (request: string, peer: string) -> string

Forwards messages of one chat to another as the account.

found_ids: [64]s64

People, bots, groups and channels matching a name, @username or +phone.

found_count: s32
Found :: (out: *Vec(u8), reply: string, comma: *string)
SearchChats :: (request: string) -> string
GroupQuery :: (request: string) -> string

Private groups may start with just their creator. Keep access hashes inside the account transport, as with every other chat operation.

TextQuery :: (request: string, peer: string) -> string

A durable outbox can retain Telegram's deduplication identity across restarts. Other callers retain a fresh cryptographically random message identity.

GroupReply :: (reply: string, selected: s64) -> string
Dispatch :: (request: string) -> string
Send :: (client: s32, request: string)
Receive :: (timeout: float64) -> string
Execute :: (request: string) -> string

Source ↗