telegram-asm
A library for writing Telegram bots in pure x86-64 assembly (NASM). Pull it in
with a single line %include "telegram.inc" and you get the whole transport: a
hand-written TLS 1.3, HTTP/1.1 with keep-alive, routing (direct or via
proxy), and wrappers around the Bot API. No libc, no external libraries — only
Linux syscalls.
Features
- Its own TLS 1.3 — X25519, AES-128-GCM, SHA-256, HKDF, entirely in assembly.
- HTTP/1.1 keep-alive with automatic reconnect on a dropped connection.
- Bot API wrappers —
sendMessage, sending photos/documents, parsinggetUpdates. - File uploads — streams
multipart/form-datastraight from disk. - URL encoding — correct UTF-8 transmission (spaces, Cyrillic, emoji).
Installation
Via nasmpkg (recommended):
nasmpkg install telegram-asm
Then it is available in your project:
%include "telegram-asm/telegram.inc"
Building your bot (nasmpkg wires include paths via NASMENV):
nasm -f elf64 -g mybot.asm -o mybot.o
ld mybot.o -o mybot
./mybot # run it where the config/ directory lives
The token goes into config/token.txt (one line).
Calling convention
Arguments go in rdi, rsi, rdx, rcx (System V order). Results come back in rax
(plus the length in rdx for tg_api_call). Functions preserve rbx, rbp, r12–r15; the scratch registers rax, rcx, rdx, rsi, rdi, r8–r11 may be clobbered.
Quick start
A minimal bot that replies hello world to /start:
%include "telegram-asm/telegram.inc"
section .rodata
tok_path: db "config/token.txt", 0
m_getupd: db "getUpdates"
s_start: db "/start"
hello: db "hello world"
hello_len equ $ - hello
section .bss
offset: resq 1
section .text
global _start
_start:
lea rdi, [tok_path]
call tg_load_token
call tg_connect
test rax, rax
js .exit
.poll:
lea rdi, [m_getupd]
mov rsi, 10
xor rdx, rdx ; no query
xor rcx, rcx
call tg_api_call ; rax=body, rdx=length
test rax, rax
jz .poll
mov rdi, rax
mov rsi, rdx
call tg_iter_init
.each:
call tg_iter_next ; rax=update_id or 0
test rax, rax
jz .poll
inc rax
mov [offset], rax ; offset = update_id + 1
call tg_update_text ; rax=ptr, rdx=length
test rax, rax
jz .each
mov rdi, rax
lea rsi, [s_start]
mov rcx, 6
call startswith
test rax, rax
jz .each
call tg_update_chatid ; rax=length -> tg_chatid_buf
lea rdi, [tg_chatid_buf]
mov rsi, rax
lea rdx, [hello]
mov rcx, hello_len
call tg_send_message
jmp .each
.exit:
mov eax, 60
xor edi, edi
syscall
For brevity the offset is not passed to getUpdates here. In a real bot build
the query offset=<update_id+1>&timeout=25 so you don't receive the same updates
again.
API reference
Connection and token
| Function | Arguments | Returns |
|---|---|---|
tg_load_token | rdi=path (NUL-terminated) | rax=0/-1; reads the file, trims the newline |
tg_set_token | rdi=ptr, rsi=length | — sets the token from memory |
tg_connect | — | rax=0/-1; routing + TLS handshake |
Requests
| Function | Arguments | Returns |
|---|---|---|
tg_api_call | rdi=method, rsi=length, rdx=query, rcx=length | rax=ptr to body, rdx=length (rax=0 → error) |
tg_api_call_post | rdi=method, rsi=length, rdx=body, rcx=length | same, but POST |
tg_send_message | rdi=chat_id string, rsi=length, rdx=text, rcx=length | rax=0/-1 |
tg_url_encode | rdi=source, rsi=length, rdx=destination | rax=length of the result |
tg_api_call builds GET /bot<token>/<method>?<query>, sends it over the
keep-alive connection, and reconnects once automatically if it was dropped.
query may be empty (rcx=0). tg_api_call_post sends the same via POST (body as
application/x-www-form-urlencoded), with no URL-length limit. tg_send_message
uses POST and URL-encodes the text.
Sending files
| Function | Arguments | Returns |
|---|---|---|
tg_send_photo | rdi=chat_id, rsi=length, rdx=path (NUL), rcx=caption, r8=caption length | rax=0/-1 |
tg_send_document | same | rax=0/-1 |
Streams the file as multipart/form-data straight from disk across multiple TLS
records (no size limit). The filename comes from the path; the caption is optional
(r8=0).
Iterating updates (getUpdates)
| Function | Arguments | Returns |
|---|---|---|
tg_iter_init | rdi=body, rsi=length | — snapshots the response, starts the iterator |
tg_iter_next | — | rax=update_id (0 = no more); sets tg_cur_ptr/tg_cur_len |
tg_update_text | — | rax=ptr, rdx=length of the text (rax=0 → none) |
tg_update_chatid | — | rax=length; digits in tg_chatid_buf (rax=0 → none) |
tg_iter_init copies the response into its own buffer so a tg_send_message in
the middle of the loop does not overwrite the body you are iterating over.
JSON helpers
| Function | Purpose |
|---|---|
json_after(hay, haylen, needle, nlen) | rax=ptr right after the substring, or 0 |
parse_uint(ptr) | rax=number, rdx=ptr past it |
extract_number(src, dst) | copies a number (with leading -) to dst, rax=length |
startswith(str, prefix, len) | rax=1/0 |
u64_to_dec(val, dst) | rax=length; decimal representation |
Requirements and limitations
- Platform: Linux x86-64 only; a CPU with AES-NI, PCLMULQDQ, RDSEED.
- The server certificate is not verified — TLS encrypts the channel, but the chain is not validated (fine for learning/personal bots; production needs your own X.509 verification).
- One connection, shared state — one API call at a time, not thread-safe.
- Substring-based JSON parsing — reliable for the flat Bot API responses, not for arbitrary nesting.