Bot Telegram adalah remote control yang praktis untuk skrip atau agen AI di mesinmu sendiri. Bot ini jalan di semua ponsel, tidak butuh aplikasi buatanmu sendiri, dan tidak butuh server publik. Masalahnya, bot itu bersifat publik. Siapa pun bisa menemukan username-nya dan mengirim pesan, dan token hanya membuktikan bahwa skripmu adalah bot tersebut; token tidak memberi tahu siapa yang menulis. Aplikasi desktop osFoundry punya jembatan seperti ini untuk agen-agennya, ditulis dengan Rust (telegram_bridge.rs). Di bawah ini desain yang sama dalam sekitar 70 baris Python pustaka standar, diuji dengan pengganti lokal Bot API, beserta kesalahan satu baris yang mengubah bot yang tenang menjadi 183.389 permintaan per menit.
Long polling, bukan webhook
Telegram mengirim pesan dengan dua cara. Webhook butuh URL HTTPS publik yang bisa dipanggil Telegram. getUpdates tidak butuh apa pun yang terbuka ke luar: skrip meminta pesan baru, dan dengan timeout=25 Telegram menahan permintaan hingga 25 detik lalu menjawab begitu ada pesan masuk. Di laptop, ini pilihan yang tepat. Tidak ada port yang harus dibuka, tidak ada tunnel, dan tidak ada sertifikat.
import json, os, secrets, time, urllib.parse, urllib.request API = os.environ.get("TG_API", "https://api.telegram.org") TOKEN = os.environ["TG_TOKEN"] LONG_POLL = 25 PAIR_TTL = 600 def call(method, params=None, body=None, timeout=40): url = f"{API}/bot{TOKEN}/{method}" if params: url += "?" + urllib.parse.urlencode(params) data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data, {"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=timeout) as r: return json.load(r) def send(chat_id, text): call("sendMessage", body={"chat_id": chat_id, "text": text[:3900]}) Timeout HTTP (40 s) harus lebih panjang daripada waktu tahan long poll (25 s). Kalau lebih pendek, setiap siklus yang sepi berakhir dengan timeout di sisi klien yang tampak seperti gangguan. Aplikasi menyimpan token di keychain OS, bukan di file. Dari Python, keyring.get_password melakukan hal yang sama, dan variabel lingkungan cukup untuk pengujian.
Dalam 60 detik tanpa aktivitas, loop long poll membuat 3 permintaan, sedangkan short poll sekali per detik membuat 60. Pesan yang masuk saat sebuah permintaan sedang ditahan mendapat balasan 1,5 sampai 6,3 ms kemudian dalam uji lokal. Di jaringan sungguhan, waktu pulang-pergi ke Telegram ditambahkan di atasnya.
Pairing: siapa yang boleh bicara
Bot harus tahu melayani siapa sebelum melakukan apa pun. Aplikasi melakukannya dengan kode sekali pakai. Kamu klik "Pair a phone" di mesin, ia menampilkan delapan karakter acak, lalu kamu kirim /pair CODE ke bot dari ponselmu. Siapa pun yang mengirim kode valid dalam 10 menit adalah pemiliknya.
pending = {} # code -> created_at paired = {} # sender id -> chat id def begin_pairing(): code = secrets.token_hex(4).upper() pending[code] = time.time() return code Kode berasal dari secrets, bukan dari sesuatu yang bisa ditebak seperti waktu atau nama bot, karena kode adalah satu-satunya hal yang mengotorisasi pengirim. Kode juga dihapus begitu dipakai, jadi hanya berlaku sekali.
Yang disimpan adalah from.id berupa angka, bukan @username atau nama tampilan. Keduanya bisa diubah, dan username yang sudah dilepas bisa didaftarkan orang lain. ID chat disimpan di sebelahnya, dengan alasan yang muncul di bawah.
Memutuskan apa yang dilakukan dengan sebuah pesan
def handle(msg, run_agent): text = (msg.get("text") or "").strip() sender = msg.get("from", {}).get("id") chat = msg["chat"] if not text or sender is None: return words = text.split() if words[0] == "/pair" or words[0].startswith("/pair@"): if chat.get("type") != "private": return send(chat["id"], "Pair with me in a direct message, not in a group.") code = words[1].upper() if len(words) > 1 else "" created = pending.pop(code, None) if created is None or time.time() - created > PAIR_TTL: return send(chat["id"], "That code is not valid or has expired.") paired[sender] = chat["id"] return send(chat["id"], "Paired. Message me and the agent will answer.") if text.lower() == "/unpair": gone = paired.pop(sender, None) is not None return send(chat["id"], "Unpaired." if gone else "You were not paired.") if paired.get(sender) != chat["id"] or chat.get("type") != "private": return # silence send(chat["id"], run_agent(text)) Tiga aturan menopang keamanannya, dan masing-masing berasal dari kegagalan tertentu yang dicatat di kode sumber aplikasi.
Pengirim yang tidak dikenal mendapat keheningan. Balasan seperti "you are not paired" memberi tahu orang asing bahwa bot terhubung ke mesin milik orang sungguhan dan mengajaknya terus mencoba. Tanpa balasan, bot tampak seperti bot yang tidak dijalankan siapa pun.
Pairing hanya bisa dilakukan di chat pribadi. Bot yang ditambahkan ke grup menerima pesan grup, jadi pairing di sana akan membuat setiap anggota bisa memberi perintah ke agen.
Orang yang sudah dipasangkan hanya dijawab di chat tempat dia melakukan pairing. Komentar di aplikasi menggambarkan versi lama yang memeriksa pengirim tetapi membalas ke chat mana pun asal pesan itu. Pengguna yang sudah dipasangkan lalu menambahkan bot ke grup, dan agen pun menjawab di grup itu, tempat semua orang bisa menjalankan alat di mesin pemilik. Otorisasi dilakukan berdasarkan pengirim dan balasan dikirim ke sebuah chat, jadi keduanya harus diperiksa bersama.
/unpair ditangani sebelum pemeriksaan chat itu, sehingga akses bisa dicabut dari ponsel, tempat kamu berada saat membutuhkannya.
Loop dan offset
def poll(run_agent): offset, failures = 0, 0 while True: try: res = call("getUpdates", {"timeout": LONG_POLL, "offset": offset, "allowed_updates": '["message"]'}) failures = 0 except Exception: failures += 1 time.sleep({1: 5, 2: 15, 3: 60}.get(failures, 300)) continue for u in res["result"]: offset = max(offset, u["update_id"] + 1) # even for messages we ignore if "message" in u: handle(u["message"], run_agent) offset adalah cara skrip memberi tahu Telegram update mana yang sudah dilihat. Apa pun di bawahnya dianggap terkonfirmasi dan dibuang, dan apa pun di angka itu atau di atasnya dikirim lagi. Komentar pada baris itu adalah bagian pentingnya. Memang menggoda untuk menggeser offset hanya setelah melakukan sesuatu dengan pesan, karena terasa seperti "sudah diproses". Tetapi pesan dari orang asing sengaja diabaikan, jadi tidak pernah dikonfirmasi, jadi muncul lagi pada panggilan berikutnya, seketika, karena ada update yang tertunda dan Telegram tidak punya alasan untuk menahan permintaan.
Backoff untuk kegagalan yang lain: token yang dicabut atau salah ketik gagal di setiap permintaan, dan mencoba ulang dengan kecepatan penuh berarti 401 setiap beberapa detik selamanya. Tangga 5, 15, 60, lalu 300 detik adalah yang dipakai aplikasi.
Apa yang ditunjukkan oleh pengujian
Pengganti lokal mengimplementasikan getUpdates (konfirmasi offset dan waktu tahan timeout) dan sendMessage, serta mencatat setiap panggilan. Setiap skenario menyuntikkan satu pesan lalu membaca apa yang dikirim balik oleh bot.
| Pesan | Balasan |
|---|---|
| orang asing: "hi" | tidak ada |
orang asing: /pair 12345678 | That code is not valid or has expired. |
saya, di grup: /pair CODE | Pair with me in a direct message, not in a group. |
saya, di DM: /pair code (huruf kecil) | Paired. |
| orang asing, memakai ulang kode saya | That code is not valid or has expired. |
| saya, di DM: sebuah pertanyaan | jawaban agen |
| saya, di grup: sebuah pertanyaan | tidak ada |
saya: /unpair, lalu sebuah pertanyaan | Unpaired., lalu tidak ada |
Lalu empat loop dijalankan selama 60 detik, masing-masing terhadap penggantinya sendiri:
| Loop | panggilan getUpdates dalam 60 s |
|---|---|
| long poll, tanpa pesan | 3 |
| short poll tiap detik, tanpa pesan | 60 |
| satu pesan orang asing, offset dimajukan untuk setiap update | 4 |
| satu pesan orang asing, offset dimajukan hanya untuk pesan yang ditangani | 183.389 |
Pada pengganti, yang menjawab dari mesin yang sama, bug ini berjalan sekitar 3.000 permintaan per detik. Terhadap API sungguhan, loop akan berjalan secepat yang diizinkan waktu pulang-pergi jaringan, dengan "hi" dari orang asing yang sama dikirim ulang di setiap panggilan dan tidak ada yang lebih baru yang masuk. Satu pesan dari seseorang yang tidak akan pernah kamu jawab sudah cukup untuk memulainya.
Yang tidak dibahas
Jembatan yang sungguhan juga perlu membatasi balasan di bawah batas 4.096 karakter Telegram (aplikasi memotong di 3.900, pada batas karakter, karena potongan per byte bisa jatuh di tengah karakter Jepang), dan mengirim balasan sebagai teks biasa. Dengan Markdown, Telegram menolak seluruh pesan jika ada tanda bintang atau garis bawah yang tidak berpasangan, dan agen yang menulis tentang kode snake_case menghasilkan persis itu. Yang terpenting, teks yang masuk ditulis oleh siapa pun yang memegang ponsel, jadi harus sampai ke agen sebagai input yang tidak tepercaya, sama seperti email. Pairing menentukan siapa yang boleh bicara dengan agen; ia tidak membuat isi pembicaraan menjadi aman.
Aplikasi desktop osFoundry menyertakan jembatan ini untuk agen AI-nya: Device → Background untuk token, dan "Pair a phone" di panel Agents sebuah kanal. Aplikasinya gratis di halaman unduh osFoundry.


