Skip to content

feat(webapp): open a bot's Mini App with real signed launch data - #295

Closed
ApexAiOfficial wants to merge 21 commits into
gotd:mainfrom
ApexAiOfficial:feat/webapp
Closed

ApexAiOfficial wants to merge 21 commits into
gotd:mainfrom
ApexAiOfficial:feat/webapp

Conversation

@ApexAiOfficial

Copy link
Copy Markdown

Adds tg webapp <bot>: it asks Telegram to open a Mini App the way a client does, so a test gets genuine initData, signed by Telegram, with no GUI client.

Opens: the menu button (default), a web app button (--text), a URL (--url) or the main Mini App (--main, with --start-param). --platform sets the reported client.

The credential: launch data lets its holder act as this account in that Mini App. It's left out of stdout and JSON unless --reveal is passed, --out writes it to a 0600 file, and parse errors never quote it.

Tests: URL and launch-data parsing, redaction, the file's mode, button selection, and that errors don't leak the credential. A real run against Shop Bot's staging bot follows.

🤖 Generated with Claude Code

ApexAiOfficial and others added 21 commits October 18, 2026 00:53
Several tg processes routinely share one session directory (parallel agent
commands, or two agents driving the same account). gotd's FileStorage and the
peer cache truncated files in place, so a concurrent reader could load an
empty or torn session or cache and fail.

- internal/atomicfile: write to a temp file in the same directory, then
  rename over the target (fsync for sessions/config; no fsync for the cache).
- File session store keeps gotd's on-disk format and path.
- Peer cache merges entries other processes saved since it was loaded, so
  concurrent commands no longer drop each other's access hashes, and treats a
  corrupt cache as empty instead of failing every command.
- writeConfig refuses to overwrite with O_EXCL instead of stat-then-write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make tg a dependable harness for agents that test Telegram bots.

Bot testing:
- Messages now carry reply_markup (inline buttons and reply keyboards, with
  callback data), entities (with the covered text), file details (kind, name,
  MIME, size, duration, dimensions), edit_date, via_bot_id and grouped_id.
  All new fields are omitted when empty, so existing JSON consumers see the
  same shape.
- tg click presses a button like a client: callback queries return the bot's
  answer (an unanswered callback is not an error), reply-keyboard buttons send
  their label, URL/copy buttons report their target. Buttons are chosen by
  label, callback data, index or row/col; errors list the available buttons.
- --wait on send, reply, click and start-bot listens before acting and
  collects the bot's replies and edits until it settles (--settle, extended
  while the bot is typing), --replies arrive, or --wait elapses.
- tg start-bot sends a deep-link /start with a parameter (messages.startBot),
  also accepting t.me/<bot>?start=<param> links.
- tg bot-info shows a bot's descriptions, command menu, menu button and
  inline/group capabilities.
- --stdin on send, reply and edit keeps secrets out of the process list.
- history --after <id> returns only newer messages.

Reliability:
- Exit codes: 2 usage, 3 auth, 4 not found, 5 flood wait, 6 timeout, 7 other
  RPC error, 130 interrupted; 1 stays the generic failure.
- With -o json a failure also writes {"schema":1,"error":{...}} to stdout
  (code, exit, message, retry_after, rpc); stderr is unchanged.
- Global --timeout (TG_TIMEOUT) bounds any command; watch ends cleanly at it.
- Flood waits are logged instead of looking like a hang.
- tg version / --version report the build, commit and whether app
  credentials are embedded.
- watch/wait register update handlers before connecting (the dispatcher map
  was modified while updates were being dispatched).
- getMessage checks the message belongs to the requested chat:
  messages.getMessages is not scoped to one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
watch and wait passed their peer argument straight to the username resolver,
so `tg wait me` failed with USERNAME_INVALID and id:<n> filters never
worked. A shared resolveManagedPeer handles me/self, id:<n> and the usual
username/phone/link forms; bot-info uses it too, so `bot-info me` reports
"not a bot" instead of a lookup failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The walk callback called info.IsDir() before checking the walk error, so
`tg upload ./missing` dereferenced a nil FileInfo and panicked. The path is
now checked before connecting, and the walk lives in a tested helper.

upload gains --wait/--settle/--replies, so a bot's handling of documents,
photos and voice notes can be verified in one call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README: fork notes, Testing bots, Exit codes, --timeout, source install.
- skills/tg: bot-testing loop for agents (bot-info, --wait, click,
  start-bot, media), result shapes, exit-code branching, rate-limit etiquette.
- CLAUDE.md: error classification, collector registration rule, JSON
  compatibility rule, atomic shared files, local toolchain paths.
- scripts/install-local.sh: build with -trimpath, embed app credentials from
  APP_ID/APP_HASH or an existing binary's build info (values never printed),
  keep versioned builds and switch <prefix>/bin/tg atomically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- TG_CONFIG sets the default --config path, matching TG_ACCOUNT, TG_PROXY and
  TG_TIMEOUT. The tg skill tells agents to pass the user's configured -c/-a on
  every call and never to run tg init/login in such a setup.
- golangci-lint 2.12.1 with Go 1.25 (the CI pair) now reports 0 issues: named
  constants for file kinds, peer aliases and command names (goconst also
  counts literals in tests), classification lists as functions, promoted
  marshalText calls, unused newRootCmd wrapper removed.
- Test pinning the send/reply JSON shape with and without --wait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go omits -ldflags from a binary's build info when building with -trimpath,
so a tg installed by install-local.sh carried no recoverable credentials and
the next run (default --creds-from ~/.local/bin/tg) failed. Build without
-trimpath, as release builds do, and refuse to install a binary whose build
info does not round-trip the credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setting a cobra version template linked text/template, whose reflective
method lookup (MethodByName) disables the linker's dead-method elimination,
so every exported method of gotd/td's generated API was kept: the binary grew
from 19 MB to 45 MB. Use cobra's template-free default ("tg version …") and
document the trap with a -dumpdep check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Atomic replacement stopped torn reads, but two tg processes that both read,
merged and replaced the cache still lost each other's entries: an
overlapping-writers test (8 writers x 25 saves) lost 18-64 of 200 entries.

internal/filelock takes an advisory exclusive lock on a sidecar <file>.lock
(flock on Unix, LockFileEx on Windows, no-op where single-process), released
by the OS if the holder dies. The peer cache holds it for the whole
read-merge-write cycle; the regression test now keeps every entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gotd re-saves the session on every (re)connect to the primary DC, re-reading
the file and writing its own auth key into it. A long-running process (e.g.
tg watch) that reconnected after another process ran tg login or tg logout
therefore wrote its old key back, undoing the login or resurrecting the
logged-out session.

The file store now remembers the auth key it loaded and, under a lock on
<file>.lock, skips a write of that key when the file was re-keyed or removed
by someone else. Same-key writers remain last-writer-wins, which is safe: the
copies differ only in volatile state such as the server salt. The guarantees
are documented on fileSessionStore, and tests cover re-login, logout,
same-key writers and this process re-keying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connectWith treats context cancellation as a clean stop, so a command
interrupted by Ctrl-C usually returned nil and tg exited 0, contradicting the
documented exit code 130. main now records which signal stopped tg and
execute reports it whatever the command returned: 130 for SIGINT, 143 for
SIGTERM (128+signal, the shell convention), with the JSON error envelope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- writeConfig created the final file with O_EXCL and then wrote into it, so a
  crash mid-write left a partial config that a retried tg init refused to
  replace. atomicfile.Create writes and syncs a temp file and hard-links it
  into place (atomic, fails if the target exists), falling back to an
  exclusive synced create on filesystems without hard links.
- Durable writes now also fsync the parent directory, so a completed rename
  survives power loss (skipped on Windows, which cannot sync a directory).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getMessage skipped the chat check for "me", so `tg download me 123` (and
click/reactions on me) returned message 123 of whichever private chat or
basic group held that id. A Saved Messages hit is now accepted only when the
response marks the message's user as the current user, without an extra RPC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collector attributed every message from the peer during the wait to the
action, but Telegram does not record which message a bot is answering. The
result now separates what can be proven:

- replies: the peer's messages ordered after the action's own message
  (observed activity; documented as such).
- linked: ids Telegram ties to the action, i.e. replies whose reply_to is the
  sent message and edits of the pressed message (deterministic).
- earlier: messages ordered before the action (message ids are monotonic per
  chat), e.g. a late answer to a previous command; never responses, and no
  longer counted toward --replies.

send/reply/start-bot/upload/click record their action once its RPC returns.
README, the tg skill and CLAUDE.md document the semantics, the narrowed
session-sharing guarantees and the 128+signal exit codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
golangci-lint goconst (which also counts test literals) flagged "version"
and "windows"; syncDir is now selected by build tags instead of a runtime
GOOS check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"interrupted by interrupt" (Go's signal String) becomes "interrupted by
SIGINT" / "interrupted by SIGTERM".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tg doctor runs the checks an agent otherwise reconstructs from exit codes:
config, selected account, app credentials (source only, never values), the
stored session (present and parseable; Keychain-aware), the peer cache, and
unless --offline a live connection and login status with round-trip time.
Each failure carries a fix hint; the result is printed in full and the exit
code is 3 when not logged in, 1 for any other failure. --account all checks
every account.

Commands that already printed their result now return reportedError so
execute adds no second error report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: bot-testing harness, stable exit codes, safely shared session files
replies is always a list; edits, linked, earlier and timed_out follow the
omitempty convention of tg's JSON. Show consumers how to read them with a
default (reported by the Scrawl-Bot session's smoke test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(doctor): diagnose config, credentials, session and connection
tg webapp asks Telegram to open a Mini App as a client does (the menu button, a web app
button, a URL, or the main app), so a test can drive the app's API or load it in a browser
with genuine initData and no GUI client. The launch data is a credential: it is printed
only with --reveal, and --out writes it to an owner-only file.
@ApexAiOfficial

Copy link
Copy Markdown
Author

Opened here by mistake (a tooling default targeted upstream instead of our fork). Closing, sorry for the noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant