ssheleg/telegram-dev

telegram-bots

Use when building, adding or auditing a Telegram bot on the HTTP Bot API: polling or webhooks, update dedup, keyboards, Telegram Stars payments, files and rate limits.

ソースを見る

使い方

  1. インストールコマンドをコピーし、自分の環境へ導入する前にソースを確認します。
  2. 必要なツール、認証情報、対応環境を原文で確認します。
  3. 入力と成果物を指定し、出力を資料と照合します。
npx skills add https://github.com/ssheleg/telegram-dev --skill telegram-bots

開始前の確認

  • 導入にはモデル契約、API枠、ファイルへのアクセス許可は含まれません。
  • スター数や導入数は活動の指標であり、独立した品質・安全性試験ではありません。
リポジトリの原文

見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。

Telegram bots — the official Bot API

Telegram holds the chat. Your database holds everything you will act on later. Every serious bot defect is an update that was processed twice, or one that was never delivered because nobody asked for it.

The Bot API is not where bots fail. They fail at the seam: the update redelivered after a crash, the member event that never arrives because of a default nobody read, the payment confirmed from the wrong signal, the broadcast that trips 429 at user 31 and silently stops.

Read against the Bot API on 2026-08-25, at version 10.3 (released 2026-08-24). Re-check `core.telegram.org/bots/api-changelog` before quoting a version: this API ships roughly monthly. Re-checked 2026-10-19: 10.3 is still the latest. For a machine to read, Telegram serves the docs as Markdown at the same path plus `.md`: `core.telegram.org/bots/api.md`, one file per method (`/bots/api/methods/getupdates.md`) and a types index (`/bots/api/types.md`). That is official and parseable, but it is Markdown, not a JSON schema.

Deep material, loaded on demand:

ReadWhen
`references/updates-and-delivery.md`wiring polling or a webhook — the delivery contract, ordering, dedup, and the migration between them
`references/payments-stars.md`taking money — Stars, invoices, the pre-checkout window, refunds, subscriptions
`references/limits-and-files.md`sending at volume, or moving files — every published limit and what to do at each
`references/frameworks.md`choosing or auditing a library — aiogram, grammY, Telegraf, python-telegram-bot, and what each hides
`references/high-load.md`the bot has real traffic, or it stops answering everyone when Telegram is slow — a queue per chat, a ceiling, payments outside it, shutdown, alarms

Runnable, and shipped beside this file: `fixtures/update_delivery.py` — nine invariants a correct handler holds, each with the mutant that makes it fail. python3 fixtures/update_delivery.py --self-test watches a redelivered update processed twice, an update lost to a crash at the offset seam, an acked update lost to a dead worker, a redelivery answered "duplicate" about work that never happened, a reply doubled by a crash before the done mark, a redelivered outbox row sent twice, one charge granted twice across two updates, a reply dropped on 429, and one payment granted twice. Standard library only. `fixtures/update_pump.py` — nine rules that keep one slow handler from holding every chat (the poll does not wait for handlers, per-chat order, a ceiling, a payment lane, flood measured from arrival, no supersession at queue time, waiting callbacks answered, dedup, a per-chat cap), each removed in turn by --self-test and watched failing.


Which API you are on, decided once

You needUseWhy
A bot users add, in chats it is inBot API — this skillHTTP, a token, no phone number, no ban risk
To read history a bot cannot see, or act as a personMTProto — telegram-userbotsa user account, with everything that costs
A web interface inside TelegramMini App — telegram-miniappsa page plus initData you must verify

A bot cannot read messages in a group without privacy mode off or a mention, see a chat it was never added to, read history from before it joined, act on behalf of a user, or download a file over 20 MB. Wanting any of those is the signal to read telegram-userbots — and to read its refusal first, because a user account is a liability a bot token is not.

The update is the event, and it arrives at least once

python
# aiogram 3.x — the shape, not the framework
async def webhook(update: Update, db) -> web.Response:
    await db.inbox_put(update.update_id, raw_body)    # INSERT on a primary key,
    return web.Response(status=200)                   # durable BEFORE the ack

# off the request path: a worker takes a pending row, does the work, marks it
# done — and a crashed attempt leaves the row pending for the next sweep
  • `update_id` is the only idempotency key you get. It is stable across

redeliveries and it identifies an update — but it is IDENTITY, not a forever guarantee of monotonicity: after ~a week idle the counter can restart from a new random base, so never compare update_ids across a long gap to decide "newer". Nothing else in an update identifies it: two identical messages a second apart are two events, and the same event delivered twice is one.

  • Inbox before ack — a claim is a receipt, not completion. The INSERT on a

primary key (never a SELECT then an INSERT: under a webhook up to max_connections, default 40, deliveries of one update can be in flight at once) writes a durable row the 200 is only then allowed to answer for. Work runs off the request under a worker lease: a worker that dies leaves the row pending and the sweep retries it. Claim-then-work inside the request is the quiet data-loss shape — the crash leaves the claim standing, Telegram's redelivery reads "duplicate", and the update is gone while every fixture stays green.

  • Updates are kept for 24 hours and no longer. A bot that is down for a day

has lost them, and nothing will say so — reconcile from your own state, never from the assumption that the queue drained.

  • Measured across eight live Telegram bots on this machine on 2026-08-25:

zero of eight deduplicate on `update_id`. Long polling hides it until the first crash between "work done" and "offset confirmed".

allowed_updates drops three types by default

python
await bot.set_webhook(
    url=f"{origin}/tg/{SECRET_PATH}",
    secret_token=WEBHOOK_SECRET,                 # 1-256 chars
    allowed_updates=["message", "callback_query", "chat_member",
                     "pre_checkout_query", "my_chat_member"],
    drop_pending_updates=False,
)

The default — an empty list, and the value you get by not passing the parameter — means "all update types except `chat_member`, `message_reaction` and `message_reaction_count`". So a bot that tracks joins and leaves receives nothing, the request returns ok: true, and the logs are clean. Name every type you handle, explicitly, and re-run setWebhook when you add one: allowed_updates is set at subscription time, not at handler time.

Webhook or polling — and never both

getUpdates will not work while a webhook is set. That is the whole migration hazard: a local getUpdates run against a production token silently takes over, or fails, depending on which side moved last. One token, one consumer.

  • **The webhook is the callback, so it needs the same defences as a payment

webhook**: verify X-Telegram-Bot-Api-Secret-Token against the value you set with secret_token, in constant time, and answer 401 with no detail when it fails. The URL is not a secret; the header is.

  • Answer fast, work later. Telegram retries on non-2xx and on a slow reply,

and a retry is a second delivery of the same update_id. Acknowledge, then process — the claim above is what makes that safe.

  • `drop_pending_updates=True` is a decision, not a cleanup. It discards

everything queued, including payments already made.

  • Long polling is correct for development and for low-volume bots; a webhook is

correct when you have a public HTTPS endpoint and care about latency. Both are in `references/updates-and-delivery.md`.

Limits are a design constraint, not an error path

LimitValueWhat it means for the design
One chat~1 message/secondbursts are tolerated, then refused
One group20 messages/minutea chatty group bot needs a queue
Bulk, all users~30 messages/seconda broadcast to 100k users is hours, not a loop
Paid broadcastsup to 1000/second0.1 Stars per message past the free 30/s

On 429 the response carries parameters.retry_after in seconds. Sleep exactly that long and retry the same call — a fixed backoff either wastes the window or trips the next one. A broadcast is a job with a rate limiter and a resume point, not a for loop; if it cannot resume, a crash at user 60 000 restarts at zero and every earlier user is messaged twice.

Money: the successful payment is the payment

Digital goods and services are sold exclusively in Telegram Stars, currency code XTR, and provider_token is left empty for them.

sendInvoice / createInvoiceLink  →  pre_checkout_query  →  successful_payment
                                    answer within 10s        deliver here
  • `answerPreCheckoutQuery` has a ten-second window. Miss it and the

transaction is cancelled — so that handler validates against your own state and nothing slow. Anything that can take a second belongs after successful_payment.

  • Deliver on `successful_payment`, never on the pre-checkout. Pre-checkout is

a question; the payment has not happened yet.

  • `refundStarPayment` exists and a refund is not a deletion. Claw back what

you granted, keyed on the charge id, exactly the way a card refund is handled — the arithmetic and the ordering are the same problem stripe-billing covers.

Full flow, subscriptions and the transaction ledger: `references/payments-stars.md`.

Files have a ceiling, and it is lower than you think

  • Download via `getFile`: 20 MB. Above that the Bot API refuses, and no

framework works around it.

  • Upload: 50 MB through the cloud Bot API.
  • A local Bot API server removes both ceilings and changes the file paths

your code receives. That is an infrastructure decision, not a flag — `references/limits-and-files.md`.

  • `file_id` is not a URL and not stable across bots. It is valid for your bot

only; store it to resend cheaply, never as an archival reference.

What belongs in your database

FactWhere it livesWhy
update_id seenyours, uniquethe only idempotency key
chat_id, user_idyoursTelegram will not list your users for you
the message you sentyours, with message_idediting later needs both ids
entitlement bought with Starsyours, keyed on the charge ida refund must find it
file_idyours, as a cachecheap resend, not an archive

Telegram is not a database and offers no way to enumerate the people who have started your bot. If you did not write it down when it happened, it is gone.

A second channel, when the audience can lose Telegram. A bot is a channel you do not own. If your users live where access to Telegram is restricted, or could become so, plan for the day it stops reaching them: key users on your own id rather than on chat_id alone, ask for a second contact (email, a web account) at a moment it is useful to the user, and keep the product reachable outside Telegram. A block then costs a channel, not the user list.

Before you ship

  1. Every handler is idempotent on `update_id` (§ The update is the event).
  2. The webhook verifies the secret header and answers 401 without detail

(§ Webhook or polling).

  1. `allowed_updates` names every type you handle — the default is not "all"

(§ `allowed_updates` drops three types).

  1. 429 sleeps for `retry_after`, and a broadcast can resume (§ Limits).
  2. Goods are delivered on `successful_payment`, not on pre-checkout

(§ Money).

  1. The poll does not wait for handlers: a queue per chat, a ceiling across

chats, payments outside it (`references/high-load.md`).

Triggers

More phrases that route here: "grammy", "telegraf", "python-telegram-bot", "getUpdates", "precheckoutquery", "high load bot", "бот тормозит", "бот апи", "Telegram-бот", "Telegram-бота", "телеграм-бота".

同じリポジトリから

関連する Skills

すべての Skills