zig-telegram is a zero-dependency, pure-Zig Telegram SDK and CLI for Zig 0.17.0 covering three protocol planes in one cohesive package:
-
Plane 1 —
telegram.bot(Telegram Bot API 10.3 Complete):-
100% of Telegram Bot API 10.3 (August 24, 2026 — 185 methods and 400 types/unions), deterministically generated from
spec/bot_api_10_3.jsonviatools/gen_bot_api.zig(zig build gen-spec/zig build spec-check). -
Recursive
attach://Multipart Engine: Comptime + runtime inspection across nested structs, optionals, slices, and unions (sendMediaGroup,sendPaidMedia,editMessageMedia,sendPoll,sendRichMessage), automatically rewriting nested.pathand.bytesInputFilefields into"attach://part_<n>"JSON references and streaming RFC 7578 binary parts with 128-bit CSPRNG boundaries. -
Single-Pass Discriminated Unions: All 23 Bot API discriminated unions (
MaybeInaccessibleMessage,ChatMember,MessageOrigin,ReactionType,BackgroundFill,BackgroundType,RevenuePartner,TransactionPartner,PaidMedia,StoryAreaType,OwnedGift,BotCommandScope,MenuButton,InputMedia,InputPaidMedia,InputProfilePhoto,InputStoryContent,RichText, etc.) parse in a single pass without intermediatestd.json.ValueDOM allocations. -
Persistent Keep-Alive HTTP Transport + MockTransport: Built on
std.http.Clientwith TLS 1.3 connection pooling, a single automatic retry of bodiless GET requests on stale keep-alive reset (POSTs are never replayed), bounded response reader (error.ResponseTooLarge), and--localBot API serverfile://path remapping. -
Caller-Owned
ErrorDetailDiagnostics: EveryBotClientmethod acceptserr_out: ?*?ErrorDetail, preservingerror_code,description,retry_after(429 rate-limit backoff), andmigrate_to_chat_idwithout thread-unsafe mutable client state. -
Bot Runtime Utilities: Resilient
Poller(automaticoffset = last_update_id + 1,429 Retry-Aftersleep, bounded Fibonacci backoff with each delay jittered to 50–100%), constant-timeWebhookProcessor(X-Telegram-Bot-Api-Secret-Tokenverified via SHA-256 +std.crypto.timing_safe.eql), and comptimeRouter(Context, routes).
-
100% of Telegram Bot API 10.3 (August 24, 2026 — 185 methods and 400 types/unions), deterministically generated from
-
Plane 2 —
telegram.mtproto(Pure-Zig MTProto 2.0 Core):-
TL Binary Codec (
mtproto.tl): 32-bit constructor IDs, 24-bit/56-bit length-prefixed strings and byte arrays with 4-byte padding alignment,Int128/Int256, andVector<T>. -
Pure-Zig MTProto 2.0 Cryptography (
mtproto.crypto): AES-256-IGE (Infinite Garble Extension) built directly onstd.crypto.core.aes.Aes256, MTProto 2.0 SHA-256msg_keyandaes_key/aes_ivKDF, and Brent–Pollard$\rho$ pqfactorization (the proof-of-work step of the DH handshake; primes, inputs ≥ 2^63, and factors ≥ 2^32 are rejected). -
Transport Framing (
mtproto.transport): Abridged (0xef), Intermediate (0xeeeeeeee), Padded Intermediate (0xdddddddd), and Obfuscated2 (64-byte header + dual CTR keystreams). -
Session Framing (
mtproto.session): Unencrypted handshake envelopes and encrypted MTProto 2.0 packets with CSPRNG padding of random length (12..1024 bytes), monotonic 64-bitmsg_id+seq_nostate, and incoming-message checks forsession_id,msg_idparity, a +30 s/−300 s time window against the server clock, and replayedmsg_ids. -
No DH key exchange yet: the
auth_keycreation handshake (req_pq_multi→req_DH_params→set_client_DH_params) is not implemented. Callers must supply an existing 256-byteauth_key(for example exported from a TDLib session).
-
TL Binary Codec (
-
Plane 3 —
telegram.tdlib(Optional TDLibtdjsonBridge):- Zero-cost runtime dynamic loader (
std.DynLib) and mockable C ABI vtable forlibtdjson(td_create_client_id,td_send,td_receive,td_execute) — requiring zero C/C++ dependencies by default.
- Zero-cost runtime dynamic loader (
Documentation & API Reference: https://guanchzhou.github.io/zig-telegram/ (Zig Autodoc /api/)
| Capability | zig-telegram |
zigtgshka |
ziogram |
telebot.zig |
|---|---|---|---|---|
| Telegram Bot API Spec | 10.3 (185 methods / 400 types) | 9.2 (161 methods) | 7.10 partial | ~30 manual methods |
| Zig Version | 0.17.0 (std.Io) |
0.14 / 0.15 | 0.14 | 0.11 |
Nested attach:// Multipart (sendMediaGroup) |
Recursive comptime + runtime | Top-level fields only | None | libcurl C FFI |
Discriminated Unions (23 unions + RichText) |
Single-pass zero-DOM scanner | 2-pass std.json.Value DOM |
Incomplete | Manual structs |
| HTTP Transport & TLS Keep-Alive | Persistent pooled std.http.Client |
New client per request | New client per request | Requires C libcurl |
Error Diagnostics (retry_after, migrate_to_chat_id) |
Caller-owned ?*?ErrorDetail |
Discarded on error | Thread-unsafe struct field | Status code only |
| Token Redaction & Constant-Time Webhook Auth | SecretToken + SHA-256 timing_safe.eql |
Raw []const u8 |
Raw []const u8 |
Raw [*c]const u8 |
| Pure-Zig MTProto 2.0 Core (TL + AES-256-IGE) | Built-in (telegram.mtproto) |
No | No | No |
Requires Zig 0.17.0.
zig fetch --save git+https://github.com/guanchzhou/zig-telegramThen wire the telegram module in your build.zig:
const telegram_dep = b.dependency("zig_telegram", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("telegram", telegram_dep.module("telegram"));const std = @import("std");
const telegram = @import("telegram");
const AppContext = struct {
client: *telegram.bot.BotClient,
pub fn onStart(self: *AppContext, msg: telegram.bot.types.Message, args: []const u8) !void {
_ = args;
var res = try self.client.sendMessage(.{
.chat_id = .fromInt(msg.chat.id),
.text = "Hello from *zig\\-telegram* \\(Bot API 10\\.3\\)\\!",
.parse_mode = .MarkdownV2,
}, null);
res.deinit();
}
};
const BotRouter = telegram.bot.Router(*AppContext, .{
.commands = &.{
.{ .name = "start", .handler = AppContext.onStart },
},
});
pub fn main(init: std.process.Init) !void {
var http: telegram.transport.HttpTransport = .init(init.gpa, init.io);
defer http.deinit();
const raw_token = init.minimal.environ.getPosix("TELEGRAM_BOT_TOKEN") orelse return error.MissingToken;
const token = try telegram.SecretToken.initValidated(raw_token);
var client = telegram.bot.BotClient.init(init.gpa, init.io, token, http.transport(), .{});
var ctx: AppContext = .{ .client = &client };
var poller = telegram.bot.Poller.init(init.gpa, &client, .{ .timeout_seconds = 30 });
try poller.run(*AppContext, &ctx, BotRouter.dispatch);
}zig-telegram automatically inspects nested slices and tagged unions for .path or .bytes InputFile values, rewrites them to "attach://part_<n>" in the JSON field, and streams the binary parts:
const media_items = [_]telegram.bot.types.InputMedia{
.{
.photo = .{
.media = .fromBytes("architecture.png", png_bytes),
.caption = "Uploaded via attach://part_0",
},
},
.{
.document = .{
.media = .fromPath("./spec/bot_api_10_3.json"),
.thumbnail = .fromBytes("thumb.jpg", jpg_bytes),
.caption = "Uploaded via attach://part_1 and attach://part_2",
},
},
};
var sent = try client.sendMediaGroup(.{
.chat_id = .fromChannel("@zig_announcements"),
.media = &media_items,
}, null);
defer sent.deinit();var processor: telegram.bot.WebhookProcessor = .{
.expected_secret_token = "whsec_production_secret_token_2026",
};
var parsed_update = try processor.verifyAndParseUpdate(
allocator,
request_headers,
request_body,
);
defer parsed_update.deinit();var client = telegram.bot.BotClient.init(allocator, io, token, http.transport(), .{
.local_server = .{
.base_url = "http://127.0.0.1:8081",
.server_files_dir = "/var/lib/telegram-bot-api",
.host_files_dir = "/mnt/telegram-data",
},
});const mtproto = telegram.mtproto;
// The DH handshake is not implemented: `auth_key` must come from an existing session.
const auth_key: [256]u8 = existing_auth_key;
var session = mtproto.session.Session.init(auth_key, server_salt, session_id);
const msg_id = session.nextMsgId(now_unix_ns);
const packet = try session.encryptMessage(allocator, io, .client_to_server, msg_id, session.nextSeqNo(true), tl_payload);
defer allocator.free(packet);
const framed = try mtproto.transport.encodeFrame(allocator, io, .intermediate, packet);
defer allocator.free(framed);
// Incoming server packets: rejects bad msg_key, session_id, msg_id parity/time, and replays.
var msg = try session.decryptMessage(allocator, .server_to_client, server_packet, now_unix_s);
defer msg.deinit(allocator);Build or run the bundled CLI:
# Print Bot API 10.3 specification coverage
zig build run -- spec
# Factor an MTProto DH pq value
zig build run -- mtproto factor-pq --pq 0x17ED48941A08F981
# Call getMe
TELEGRAM_BOT_TOKEN="123456:ABC-DEF" zig build run -- me
# Send a message
TELEGRAM_BOT_TOKEN="123456:ABC-DEF" zig build run -- send --chat 42 --text "Hello!" --parse-mode MarkdownV2
# Long-poll updates as JSONL
TELEGRAM_BOT_TOKEN="123456:ABC-DEF" zig build run -- poll --timeout 25
# Call any of the 185 Bot API 10.3 methods with raw JSON
TELEGRAM_BOT_TOKEN="123456:ABC-DEF" zig build run -- call getWebhookInfo '{}'Run zig build bench or enforce the regression gate with zig build perf-check:
zig-telegram ReleaseFast benchmark (25000 iterations)
--------------------------------------------------------------------
Discriminated union single-pass : 740 ns/op
Discriminated union 2-pass DOM : 863 ns/op (1.17x speedup)
Full Bot API Update envelope : 1696 ns/op
MTProto 2.0 AES-256-IGE (1 KiB) : 979.2 MiB/s
Webhook SHA-256 constant-time : 34 ns/op
zig build test # Unit, golden, surface probe, OOM, CLI, and example builds
zig build spec-check # Verify src/bot/*.zig matches spec/bot_api_10_3.json
zig build perf-check # Run ReleaseFast performance regression gate
zig build docs # Generate landing page + Zig autodoc in zig-out/docs/
mkdir -p .zig-cache/tmp && zig build test -Dllvm=true --fuzz=50K- Redacted
SecretToken:SecretToken.formatalways emitsbot<id>:[REDACTED]so bot tokens cannot leak viastd.logor formatting calls. - Constant-Time Webhook Authentication:
X-Telegram-Bot-Api-Secret-Tokenis hashed with SHA-256 on both sides and compared viastd.crypto.timing_safe.eql([32]u8, a, b)to eliminate length and content timing side channels. - RFC 7578 Multipart Hardening: 128-bit CSPRNG multipart boundaries (
std.Io.random) and strict filename/field-name quote and CRLF escaping prevent multipart header injection. - Bounded Memory & Path Traversal Protection:
max_response_bytes(default 16 MiB) prevents unbounded response allocation, andLocalServerConfig.resolveLocalFilePathrejects..traversal segments.
MIT License — Copyright (c) 2026 Andrey Maltsev. See LICENSE.