feat: telegram bot /briefing /watchlist /thesis + multi-chat auth + rate limit (#165)#173
Open
luceinaltis wants to merge 2 commits into
Open
feat: telegram bot /briefing /watchlist /thesis + multi-chat auth + rate limit (#165)#173luceinaltis wants to merge 2 commits into
luceinaltis wants to merge 2 commits into
Conversation
…ate limit Extends the Server-side Telegram integration so a remote user can get the same session-start briefing, watchlist prices, and saved trade theses they would see in the REPL, and so a shared bot can safely authorise more than one chat without abandoning sender-targeted replies. - TelegramBotPoller: new `allowed_chat_ids` kwarg (primary chat always included, dedupes / trims blanks) and sliding-window per-chat rate limit (default 20 commands / 60s, configurable). BotCommand now carries the sender's `chat_id`; `send_reply(text, chat_id=...)` routes replies back to whoever asked and falls back to the primary chat for unauthorised ids. - notifications/factory: `build_telegram_poller` reads `TELEGRAM_ALLOWED_CHAT_IDS` (comma-separated) and threads it into the poller; logs the authorised chat count when > 1. - Server: new `watchlist`, `data_registry`, `sessions_dir`, `reports_dir` kwargs. `_dispatch_bot_command` is async now and handles three new actions — `/briefing` (delegates to `generate_briefing`), `/watchlist` (lists tickers with live prices, tolerant of feed failures), and `/thesis` (scans `reports_dir` for the three most recent reports with a `## Trade Thesis` section). `_handle_bot_command` forwards `command.chat_id` to `send_reply` so replies go back to the sender. - cli.py `serve`: builds a shared `Watchlist` up front, passes watchlist / data_registry / sessions_dir / reports_dir into `Server`, and surfaces the authorised chat count on startup. - Tests: 9 new poller tests (multi-chat + rate limit + `BotCommand.chat_id` plumbing), 8 new factory tests (`TELEGRAM_ALLOWED_CHAT_IDS` parsing), 14 new server tests (/briefing + /watchlist + /thesis handlers + chat-routed replies). Existing dispatch tests updated to await the now async handler. Closes part of #165. Routing arbitrary free-text to `ConversationEngine.query` still requires wiring an engine (plus its session logger / memory searcher / summaries dir) into `qracer serve`; that's deferred to a follow-up so this PR stays focused on the bot surface that doesn't need a full analytical pipeline. https://claude.ai/code/session_01M88YFVX5Ez393LjQLPNLzH
Pure formatting. CI's `ruff format --check .` flagged these four files as needing reformatting; `ruff format` applied line-wrap adjustments (no behavioural changes). Keeps the code-quality job green. https://claude.ai/code/session_01M88YFVX5Ez393LjQLPNLzH
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes part of #165.
Issue #165 asks for a complete Telegram conversational surface. Most of the plumbing was already in place on
main—TelegramBotPollerwired intoServer._tick(), autonomous alerts routed throughNotificationRegistry, and basic/help/status/alerts/alert/tasks/schedulecommands. The gaps this PR closes:/briefing,/watchlist,/thesiscommands are now live on the Telegram bot, reusing the existing session-briefing machinery, price provider, and saved-report directory.TELEGRAM_ALLOWED_CHAT_IDSso a bot shared between multiple users can authorise each chat while still replying to the sender.What changed
qracer/notifications/telegram_poller.pyBotCommandgains achat_idfield — the sender's chat — so replies can target the right chat when more than one is authorised.TelegramBotPoller.__init__acceptsallowed_chat_ids: list[str] | None. The primarychat_idis always authorised; extras are deduped and blanks are dropped. Exposed viaallowed_chat_idsproperty._RateBucket(sliding-windowdeque[float]per chat) gated byrate_limit_commands/rate_limit_window_seconds. Over-limit commands are logged and dropped frompoll(), but the update offset still advances so we never re-fetch them.rate_limit_commands=0disables traffic.send_reply(text, chat_id=None)— unauthorised targets fall back to the primary chat with a warning; no kwarg keeps previous behaviour.qracer/notifications/factory.pybuild_telegram_pollerreadsTELEGRAM_ALLOWED_CHAT_IDS(comma-separated), parses out blanks, threads it into the poller, and logs(authorised chats: N)whenN > 1.qracer/server.pyServer.__init__acceptswatchlist,data_registry,sessions_dir,reports_dir._dispatch_bot_commandis nowasync— handlers returning dynamic data can await data-registry / briefing calls._cmd_briefingdelegates toqracer.conversation.quickpath.generate_briefing. Returns a graceful hint when deps are missing, when there's no prior session, or when the helper raises._cmd_watchlistfetches prices viaPriceProvider, tolerates per-ticker feed failures ("X: price unavailable"), and gracefully degrades to a ticker-only list when nodata_registryis configured._cmd_thesisscansreports_dirfor.mdfiles containing a## Trade Thesissection (newest first), takes up to three, and extracts the thesis body via a new_extract_thesis_sectionhelper (capped at 800 chars, truncated at the next##/---boundary)._handle_bot_commandnow forwardscommand.chat_idintosend_reply, so replies go back to whichever authorised chat issued the command.qracer/cli.py::serveWatchlistunconditionally (previously only whenautonomous_enabled).watchlist,data_registry,sessions_dir=_user_dir()/"sessions",reports_dir=_user_dir()/"reports"intoServer(...).Scope mapping (#165)
TelegramBotPollerintoqracer serveevent loopmainConversationEngine.query()ConversationEngine(plus session logger / memory searcher / summaries dir) wired intoserve; sized as a follow-upNotificationRegistryonmain/briefingTelegram command/watchlistTelegram command/thesisTelegram commandreports_dir; will be upgraded to FactStore-backed once #157/#158 land)telegram.bot_tokenconfigTELEGRAM_BOT_TOKENcredential)telegram.allowed_chat_idsconfigTELEGRAM_ALLOWED_CHAT_IDScredentialThe single deferred item (engine routing) is called out explicitly rather than shipped half-done — it would require pulling the REPL's
SessionLogger/MemorySearcher/ summaries-dir setup into theservecommand, which is out of scope for a focused bot-commands PR.Test plan
uv run pytest— 801 passed, 14 skipped (31 new tests).uv run ruff checkon changed files — clean.uv run pyright qracer/server.py qracer/notifications/telegram_poller.py qracer/notifications/factory.py— 0 errors.New test coverage
tests/notifications/test_telegram_poller.py(9 new)allowed_chat_idsdefaults to primary-only, dedupes, drops blanks, still authorises the primary.poll()accepts messages from secondary authorised chats and still rejects unauthorised ones.send_reply(chat_id=...)routes to the requested chat; unauthorised target falls back to primary; default routes to primary.rate_limit_commands=0blocks everything.BotCommand.parserecords the supplied chat id and coerces tostr.tests/notifications/test_factory.py(8 new)build_telegram_pollerreturnsNonewithout credentials, parses a comma-separatedTELEGRAM_ALLOWED_CHAT_IDS, trims blanks, dedupes primary, keeps primary when only the primary is configured.tests/test_server.py(14 new)/briefinghandler: missing deps hint, no-prior-session hint, success path, exception path./watchlist: unconfigured hint, empty, no-data-registry ticker-only output, prices, per-ticker feed failure, non-numeric price fallback./thesis: no reports dir, empty reports dir, no thesis section, extraction of the thesis body without leakingData Sources, capping at three entries.command.chat_idis passed through tosend_reply; a blankchat_idbecomesNoneso the poller's primary is used.Manual verification
https://claude.ai/code/session_01M88YFVX5Ez393LjQLPNLzH