Type: bug
Area: Telegram bot
Severity: high — silently loses user commands; blocks inline keyboards entirely
Addressed by: PR 2 of 4 (feat(telegram): keyboard primitives and update-parser rewrite)
Summary
Two related defects in the incoming-update path. Both are silent — no error is
logged in either case.
Bug A — callback_query updates are permanently swallowed
The handler does:
r.get('message', {}).get('text', '')
A callback_query update has no message key at that position, so this yields
an empty text and an empty chat id. The loop guard only checks the update id:
[ -z "$_uid" ] && continue
so _process_cmd runs on empty input, writes the offset, and Telegram is
told the update was consumed. The callback is never redelivered.
Impact: this is currently latent because nothing sends inline keyboards. It
becomes an immediate hard blocker for any inline-keyboard feature — every button
tap would be acknowledged away and do nothing.
Bug B — the no-python3 fallback drops updates
On a host without a working python3, the fallback extracts the text and the
chat id in two independent passes:
_text=$(echo "$updates" | grep -oE '"text"\s*:\s*"[^"]*"' | tail -1 | sed ...)
_cid=$(echo "$updates" | grep -oE '"chat"\s*:\s*\{[^}]*"id"\s*:\s*-?[0-9]+' | tail -1 | ...)
Then pairs them by position. Consequences:
- Every update but the last in a batch is dropped. Telegram delivers
batched updates whenever more than one message arrives between polls (for
example while the bot is restarting). Those commands vanish.
- Text and chat id can be paired across updates. In a two-update batch,
update B's text can execute against update A's chat id.
"chat"\s*:\s*\{ requires chat and { to be adjacent, so a reordered
object fails to match even for a single update.
- The update offset is passed as the
update_id argument to _process_cmd.
Impact: this is not a corner case. Alpine is a supported platform (OpenRC
support landed in #130) and typically ships without python3, so the fallback is
the only path there.
Reproduction (Bug B)
Send two messages to the bot in quick succession while it is between polls. Only
the second is processed. Note that command -v python3 is not a reliable guard:
the Windows Store ships a python3.exe alias that is on PATH and fails when
executed.
Proposal
Have both extractors emit the same normalised records:
<update_id>\t<kind>\t<chat_id>\t<message_id>\t<callback_id>\t<payload>
with kind ∈ msg|cb, so a single consumer dispatches both. A record should be
emitted for every element of result — including ones carrying no message —
because the consumer advances the offset per record, and skipping one would make
Telegram redeliver it forever.
The fallback must be a character scanner, not a regex pass: a regex cannot
tell whether a } or " sits inside a string literal, and a command's text can
legitimately contain both.
Also:
- Probe
python3 by executing it, not with command -v.
- Move the offset write out of
_process_cmd into the consumer, so it advances
per consumed record and a mid-batch failure redelivers the tail rather than
losing it.
- Read the records with process substitution rather than a pipe, so dispatchers
run in the main shell.
Acceptance criteria
Type: bug
Area: Telegram bot
Severity: high — silently loses user commands; blocks inline keyboards entirely
Addressed by: PR 2 of 4 (
feat(telegram): keyboard primitives and update-parser rewrite)Summary
Two related defects in the incoming-update path. Both are silent — no error is
logged in either case.
Bug A —
callback_queryupdates are permanently swallowedThe handler does:
A
callback_queryupdate has nomessagekey at that position, so this yieldsan empty text and an empty chat id. The loop guard only checks the update id:
so
_process_cmdruns on empty input, writes the offset, and Telegram istold the update was consumed. The callback is never redelivered.
Impact: this is currently latent because nothing sends inline keyboards. It
becomes an immediate hard blocker for any inline-keyboard feature — every button
tap would be acknowledged away and do nothing.
Bug B — the no-python3 fallback drops updates
On a host without a working
python3, the fallback extracts the text and thechat id in two independent passes:
Then pairs them by position. Consequences:
batched updates whenever more than one message arrives between polls (for
example while the bot is restarting). Those commands vanish.
update B's text can execute against update A's chat id.
"chat"\s*:\s*\{requireschatand{to be adjacent, so a reorderedobject fails to match even for a single update.
update_idargument to_process_cmd.Impact: this is not a corner case. Alpine is a supported platform (OpenRC
support landed in #130) and typically ships without
python3, so the fallback isthe only path there.
Reproduction (Bug B)
Send two messages to the bot in quick succession while it is between polls. Only
the second is processed. Note that
command -v python3is not a reliable guard:the Windows Store ships a
python3.exealias that is onPATHand fails whenexecuted.
Proposal
Have both extractors emit the same normalised records:
with
kind∈msg|cb, so a single consumer dispatches both. A record should beemitted for every element of
result— including ones carrying no message —because the consumer advances the offset per record, and skipping one would make
Telegram redeliver it forever.
The fallback must be a character scanner, not a regex pass: a regex cannot
tell whether a
}or"sits inside a string literal, and a command's text canlegitimately contain both.
Also:
python3by executing it, not withcommand -v._process_cmdinto the consumer, so it advancesper consumed record and a mid-batch failure redelivers the tail rather than
losing it.
run in the main shell.
Acceptance criteria
callback_queryupdates produce acbrecord carrying the callback id,the message id and the payload.
"chat":{"id":999}does not corrupt parsing.