Skip to main content

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 wrapperssendMessage, sending photos/documents, parsing getUpdates.
  • File uploads — streams multipart/form-data straight 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
note

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

FunctionArgumentsReturns
tg_load_tokenrdi=path (NUL-terminated)rax=0/-1; reads the file, trims the newline
tg_set_tokenrdi=ptr, rsi=length— sets the token from memory
tg_connectrax=0/-1; routing + TLS handshake

Requests

FunctionArgumentsReturns
tg_api_callrdi=method, rsi=length, rdx=query, rcx=lengthrax=ptr to body, rdx=length (rax=0 → error)
tg_api_call_postrdi=method, rsi=length, rdx=body, rcx=lengthsame, but POST
tg_send_messagerdi=chat_id string, rsi=length, rdx=text, rcx=lengthrax=0/-1
tg_url_encoderdi=source, rsi=length, rdx=destinationrax=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

FunctionArgumentsReturns
tg_send_photordi=chat_id, rsi=length, rdx=path (NUL), rcx=caption, r8=caption lengthrax=0/-1
tg_send_documentsamerax=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)

FunctionArgumentsReturns
tg_iter_initrdi=body, rsi=length— snapshots the response, starts the iterator
tg_iter_nextrax=update_id (0 = no more); sets tg_cur_ptr/tg_cur_len
tg_update_textrax=ptr, rdx=length of the text (rax=0 → none)
tg_update_chatidrax=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

FunctionPurpose
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.