mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-11 03:13:46 +00:00
Telegram expires an inline query and then rejects the answer with "query is too old and response timeout expired or query ID is invalid". The picker invited that: it listed the names and then read the store once per name — up to 50 round trips per keystroke — and it was the only handler in the module with no deadline of its own. Updates are dispatched one at a time, so a single slow answer also held up the queries queued behind it, each ageing while it waited, and one slow read expired a whole burst of typing. Add DocStore.Scan, which reads a key prefix with its values in one round trip, ordered by key. The picker and /aliases both use it, so neither grows a round trip per saved alias. Bound the inline handler at 3s: an answer later than that is rejected anyway, and giving up frees the worker for the fresher query behind it. When Telegram does reject an answer, the error now carries how long it took, which separates a slow handler from a query that was already stale on arrival. The 50-result cap now counts results the picker can show, so a video-note alias — which has no cached inline type — no longer consumes a slot.
225 lines
9.6 KiB
Markdown
225 lines
9.6 KiB
Markdown
# Aliases
|
|
|
|
The `alias` module lets anyone give a short name to a message and send it back
|
|
later by that name.
|
|
|
|
| Command | Parameters | Reply to | What it does |
|
|
|---|---|---|---|
|
|
| `/alias` | `<name>` | any supported message | Saves it under that name |
|
|
| `/insert` | `<name>` | — | Sends back whatever is saved under it |
|
|
| `/aliases` | — | — | Lists every saved name |
|
|
| `/unalias` | `<name>` | — | Deletes a saved name |
|
|
| `/<name>` | — | — | Same as `/insert <name>` |
|
|
| `@botname <prefix>` | — | — | Inline picker, in any chat |
|
|
|
|
All are public and single-shot.
|
|
|
|
## The namespace is global
|
|
|
|
A name assigned in any chat works in **every** chat, for **everyone** — the same
|
|
way the sticker pack `/addsticker` writes to is shared. The store is a plain map
|
|
from name to content, with no chat or user in the key.
|
|
|
|
The consequence is worth stating plainly: anyone can reassign anyone's name.
|
|
`/alias` overwrites rather than refusing, and its reply says what it replaced —
|
|
|
|
> Replaced /insert cheer — it was a sticker, now it is a GIF.
|
|
|
|
Overwriting is deliberate: refusing would make a mistyped alias awkward to
|
|
correct. `/unalias <name>` deletes one, and is open to anyone for the same
|
|
reason overwriting is — the namespace is shared, so the permission model is too.
|
|
A per-owner restriction would leave an alias whose assigner has left the chat
|
|
permanently unremovable.
|
|
|
|
## Invoking an alias
|
|
|
|
Three ways, same binding:
|
|
|
|
1. **`/<name>`** — a saved name works as its own command. `/cheer` is `/insert cheer`.
|
|
2. **`/insert <name>`** — always works, including when inline mode is off.
|
|
3. **`@botname <prefix>`** — an inline picker with previews, usable in any chat,
|
|
even ones the bot is not a member of.
|
|
|
|
**Code always beats an alias.** The dispatcher registers every real command
|
|
before the alias fallback, and the bot library returns the *first* handler whose
|
|
matcher accepts an update — so a command defined in code can never be shadowed
|
|
by a name resolved at runtime. `/alias` refuses a name that is already a
|
|
command for the same reason, since such an alias would only ever be reachable
|
|
through `/insert`.
|
|
|
|
If a future build adds a command whose name an alias already uses, the command
|
|
silently wins and the alias stays reachable via `/insert`. That is the intended
|
|
precedence, not a bug to fix.
|
|
|
|
**An unknown `/command` is answered with silence.** The fallback sees every
|
|
unrecognised command in every chat the bot is in, so replying would turn a typo
|
|
like `/pign` into noise, and would confirm to anyone probing which names exist.
|
|
|
|
## Inline mode
|
|
|
|
`@botname` with no query lists everything; with a query it filters by name
|
|
prefix, case-insensitively, sorted, capped at Telegram's 50 results per answer.
|
|
|
|
Each result is a **cached** inline type — it carries the `file_id` Telegram
|
|
already holds, so nothing is uploaded and the picker shows real previews. This
|
|
is the payoff for storing a `file_id` rather than bytes.
|
|
|
|
**Video-note aliases do not appear inline.** Telegram defines no
|
|
`InlineQueryResultCachedVideoNote`, and substituting a plain video would change
|
|
what was saved. They stay reachable through `/insert` and `/<name>`. The 50-cap
|
|
counts results the picker can show, so a skipped kind does not eat a slot.
|
|
|
|
**Answering is a race, and losing it is silent.** Telegram expires an inline
|
|
query and then rejects the answer with
|
|
|
|
> Bad Request: query is too old and response timeout expired or query ID is
|
|
> invalid
|
|
|
|
Every keystroke opens a *new* query, and the bot dispatches updates one at a
|
|
time, so one slow answer also delays the queries queued behind it — each ageing
|
|
while it waits. One slow read can therefore expire a whole burst of typing.
|
|
Two rules keep that from happening:
|
|
|
|
- The handler reads the store **once** per query (`Scan`), never a name listing
|
|
followed by a read per name. An N+1 read costs a round trip per saved alias,
|
|
on every keystroke.
|
|
- It runs under a 3-second deadline, not the 10 seconds the commands get. An
|
|
answer that late is rejected anyway; abandoning it frees the worker for the
|
|
fresher query behind it.
|
|
|
|
When the rejection does appear, the log line carries how long the answer took —
|
|
`answer 4 results after 12.4s: ...` — which separates a slow handler from a
|
|
query that was already stale on arrival.
|
|
|
|
**Two things gate inline mode, and both fail silently.**
|
|
|
|
1. `inline_query` must be in `pollingAllowedUpdates`
|
|
(`internal/telegram/client.go`). Telegram filters getUpdates server-side, so
|
|
a missing kind means the handler is never called — no log line, no error.
|
|
2. Inline mode must be enabled for the bot in BotFather (`/setinline`). Until
|
|
it is, Telegram does not offer the bot for inline use at all, so typing
|
|
`@botname` shows nothing. This is a one-time operational step that cannot be
|
|
done from code.
|
|
|
|
## Names
|
|
|
|
One word, username-shaped: starts with a letter, then letters, digits and
|
|
underscores, up to 32 characters. A leading `@` is stripped rather than
|
|
rejected, since these names imitate usernames and typing the sigil is a natural
|
|
slip.
|
|
|
|
Lookups fold case — `/insert LOUD` and `/insert loud` find the same entry — and
|
|
the spelling the assigner used is what gets echoed back.
|
|
|
|
Telegram's own username minimum is 5 characters; this allows 1 on purpose. The
|
|
point of an alias is to be shorter than what it replaces, and `gg` is a good
|
|
name for a sticker.
|
|
|
|
## What can be saved
|
|
|
|
| Replied message | Sent back with |
|
|
|---|---|
|
|
| Sticker | `sendSticker` |
|
|
| Photo | `sendPhoto` |
|
|
| GIF / animation | `sendAnimation` |
|
|
| Video | `sendVideo` |
|
|
| Video note | `sendVideoNote` |
|
|
| Audio | `sendAudio` |
|
|
| Voice message | `sendVoice` |
|
|
| File / document | `sendDocument` |
|
|
| Plain text | `sendMessage` |
|
|
|
|
Anything else — a location, a poll, a contact — is refused with the list above.
|
|
|
|
**Nothing is downloaded.** Every media kind is kept as the `file_id` Telegram
|
|
already issued, and `/insert` hands that same id straight back to a send call.
|
|
The module stores bytes for nothing but the name and a caption. A `file_id`
|
|
refers to a file on Telegram's servers, so an alias survives restarts and
|
|
redeploys.
|
|
|
|
The kind is stored alongside the id because a bare `file_id` does not say which
|
|
send method will accept it.
|
|
|
|
**Order matters when Telegram fills more than one field.** A GIF arrives as an
|
|
`Animation` *and* a `Document`, and the more specific kind is claimed first —
|
|
otherwise `/insert` would hand back a plain file instead of a looping GIF.
|
|
|
|
**Captions come back too**, for the kinds that can carry one. Stickers and video
|
|
notes cannot, and Telegram's send methods for them have no caption field at all.
|
|
|
|
## Listing
|
|
|
|
`/aliases` prints the count and every name, sorted, in one message.
|
|
|
|
**Names only, not what each holds.** The store answers "which keys exist" in a
|
|
single call, while naming each kind would cost one read per alias — a round trip
|
|
each against MongoDB, on a dispatcher that serves one update at a time. To find
|
|
out what a name holds, `/insert` it.
|
|
|
|
One line per alias, showing what the name holds, with the invocation in a
|
|
`<code>` span so tapping it copies a command ready to send:
|
|
|
|
```
|
|
3 aliases:
|
|
/cheer — sticker
|
|
/clip — video
|
|
/greeting — text
|
|
```
|
|
|
|
Names list in their folded (lowercase) form, which is exactly what `/insert`
|
|
takes.
|
|
|
|
This is one store read total — `DocStore.Scan` returns the names with their
|
|
documents, and the kind that labels each line lives in the document. The list is
|
|
trimmed to Telegram's 4096-character limit and ends with `…and N more.`; the
|
|
count at the top is always the true total.
|
|
|
|
## Behaviour worth knowing
|
|
|
|
**Formatting survives.** Bold, italic, code, links and mentions are stored as
|
|
entities alongside the text, and captions keep theirs too. This works because
|
|
the text is re-sent byte-identical: entity offsets are relative to that text,
|
|
so they stay valid. They are sent back as entities rather than re-rendered as
|
|
markup, which avoids escaping and re-parsing content the user never wrote as
|
|
markup.
|
|
|
|
**Another bot's message cannot be saved.** Telegram's own rule: *"Bots will not
|
|
be able to see messages from other bots regardless of mode."* The reply arrives
|
|
with its content stripped, so there is nothing to store and no setting that
|
|
would change it. `/alias` says so specifically rather than implying the format
|
|
was unsupported. Forwarding the message to yourself first and aliasing your own
|
|
copy works.
|
|
|
|
**A `file_id` can stop working** — the original file was deleted, or Telegram
|
|
rejects it. `/insert` answers with something actionable rather than a generic
|
|
failure:
|
|
|
|
> "gone" can no longer be sent. Save it again with /alias gone.
|
|
|
|
**Replies keep their forum topic.** Every send forwards `MessageThreadID`, for
|
|
the reason `chathelper.Reply` documents: without it Telegram routes the message
|
|
to a supergroup's General topic instead of the topic the command was typed in.
|
|
|
|
## Debugging a capture
|
|
|
|
Set `LOG_LEVEL=debug` and every `/alias` logs one `alias_capture` line
|
|
describing what Telegram actually delivered:
|
|
|
|
```
|
|
alias_capture reply=present reply_id=9 fields=none text_len=0 caption_len=0
|
|
entities=0 captured=false from_id=555 from_bot=true
|
|
```
|
|
|
|
`fields=none captured=false from_bot=true` is the signature of another bot's
|
|
message arriving stripped. `reply=absent` means Telegram delivered the command
|
|
with no reply attached at all — indistinguishable from the caller forgetting to
|
|
reply, which is why the line exists.
|
|
|
|
It reports **shape, never content**: field names, lengths and counts, but no
|
|
message text. The line lands in stdout and whatever ships it, so aliased
|
|
messages must not travel with it; a test asserts nothing leaks.
|
|
|
|
Both command handlers run under a 10-second deadline; the inline handler under
|
|
3. The bot processes updates one at a time, so those bounds are what keep a slow
|
|
store from stalling other users.
|