Lessons 1 and 2 used words like chat id, message id, and card in passing. This lesson stops and defines every noun properly, then draws the map of how they point at each other — because a bot's entire architecture is really just app state hung off Telegram's ids. There's now a glossary for these terms; from here on, every lesson sticks to it.
The teach card's cursor position — which of the three kinds of state is it?
A bot is just an account on Telegram — a peer that can be in chats, send and
receive messages — except a program drives it instead of a phone. You create one by talking to
BotFather (Telegram's bot for
making bots), and what you get back is a token: one string, something like
7213...:AAH9f..., that is simultaneously the bot's identity and its
password. Every API call your code makes embeds this token — that's the entire
authentication story, and it's why the token lives in env and never in code.
Note what this means for this repo: TELEGRAM_BOT_TOKEN and
TELEGRAM_DEV_BOT_TOKEN are two different bots — two separate Telegram
accounts with separate usernames, chats, and update queues. Same brain
(createBot), two bodies. That's also why dev long-polling can run while prod's
webhook stays registered: each bot has its own queue, so they never fight.
A chat is one conversation — you-and-the-bot, a group, a channel. Telegram
gives every chat a permanent numeric chat.id, and that number is
the bot's postal address for it: sending anything means
sendMessage(chatId, …).
A subtlety worth engraving: the chat id identifies the conversation, not the
person — even though in a private 1-on-1 chat Telegram deliberately sets the chat id
equal to the other party's user id. That's true across all bots: your
private chat with the dev bot and with the prod bot carry the same number — yours. What keeps
them apart is the token the API call is made with, so the full address of a private
conversation is really (bot token, chat id). The two concepts still diverge the
moment a group is involved: a group chat has its own (negative) id shared by all members, and
the same human talking to your bot privately and in a group shows up under two different
chat ids. w2tAgent only ever deals with one private chat, so
pairing is a single link:
telegramLinks/{chatId} → userId — Telegram's address on one side, your app's
identity on the other. That translation is the whole point of the pairing table.
A message is one bubble in a chat. Every message gets a
message_id — but here is the trap:
message ids are unique only within their chat, they're just a counter that increments
per conversation. Message #1042 exists in millions of chats at once. So the true, global
address of any bubble on Telegram is the pair
(chat_id, message_id) — which is exactly the signature of the edit API:
editMessageText(chat_id,
message_id, new_text).
Two more facts you'll lean on constantly: a bot can edit its own messages (not the user's), and the id of a message the bot just sent comes back in the API response — you don't invent ids, Telegram assigns them and tells you:
server/telegram/teach.tsconst message = await api.sendMessage(link.chatId, text, { … })
const session: TeachSession = {
chatId: link.chatId,
activeMessageId: message.message_id, // ← Telegram just assigned this
cursor: 0,
…
}
An inline keyboard is a grid of buttons attached to one specific message —
part of the bubble itself, not of the chat. Each button carries a hidden string,
callback_data, at most
64 bytes. Lesson 2's
rule follows directly: the button carries a verb (teach:next) or
coordinates (ans:2:1:0), never state.
When a user taps an inline button, no message is sent. Instead Telegram creates a callback query — a little event object that answers the three questions your handler needs:
What was tapped? → the button's callback_data.
Where was it tapped? → the full message the keyboard was attached to,
including its message_id.
Who tapped? → the tapping user.
That middle answer is the linchpin of this repo's whole session design. The tap arrives already knowing which bubble it happened on — so a verb-only button plus the tapped message's id is enough to find the right state:
server/telegram/teach.tsconst messageId = ctx.callbackQuery?.message?.message_id
const session = await getTeachSessionByMessage(scope, messageId)
// … mutate cursor, save, then re-render THAT bubble:
await ctx.editMessageText(text, { reply_markup: keyboard })
One duty comes with every callback query: the client shows a spinner until the bot calls
answerCallbackQuery, so the handler must always answer — even for stale buttons
and unauthorized chats (that's the onCallback wrapper in
server/telegram/bot.ts).
Everything above — a text message arriving, a button tapped — reaches your code wrapped in an
update: Telegram's
delivery envelope, with a sequence number (update_id) and exactly one payload
inside (either a message or a callback query or …). Updates are what
lesson 1 was about moving: the webhook POSTs one update per request; long polling pulls them in
batches. grammY opens the envelope and routes the payload to bot.on('message:text')
or bot.callbackQuery(…) for you.
Now the word we've been using loosely. Telegram has no "cards" — it has messages. A card is an application pattern built from three entities you now know:
editMessageText on every tap. The message is the
screen, the keyboard is the controls, the session doc is the program
state — a tiny app living inside one chat bubble.
That's why w2tAgent's teach sessions are named teach-{messageId}: one screen, one
state doc, joined by the id Telegram assigned at sendMessage time. Every past
day's card keeps working forever because each bubble can find its own state.
Read the arrows on the right and you'll see the design rule of the last lesson again from a new angle: every piece of app state is keyed by a Telegram id. Identity state hangs off the chat id; per-card UI state hangs off the message id. When you build your next bot, choosing those keys is designing the bot.
1 · Your bot logged "message_id: 1042" yesterday and you want to edit that message now. Is the id enough?
2 · A user taps ▶ on a teach card from four days ago. How does the bot know to move THAT card's cursor and not today's?
3 · Same word, both bots: you send "Hund" to the dev bot and to the prod bot. How many chats, and how do their chat ids compare?
Anything fuzzy — group chats and negative ids, what else rides in an update, how grammY's
ctx maps onto these objects? Ask your teacher (me) directly, any time.