Skip to content

feat: telegram bot /briefing /watchlist /thesis + multi-chat auth + rate limit (#165)#173

Open
luceinaltis wants to merge 2 commits into
mainfrom
feat/165-telegram-bot-commands
Open

feat: telegram bot /briefing /watchlist /thesis + multi-chat auth + rate limit (#165)#173
luceinaltis wants to merge 2 commits into
mainfrom
feat/165-telegram-bot-commands

Conversation

@luceinaltis
Copy link
Copy Markdown
Owner

Summary

Closes part of #165.

Issue #165 asks for a complete Telegram conversational surface. Most of the plumbing was already in place on mainTelegramBotPoller wired into Server._tick(), autonomous alerts routed through NotificationRegistry, and basic /help /status /alerts /alert /tasks /schedule commands. The gaps this PR closes:

  • /briefing, /watchlist, /thesis commands are now live on the Telegram bot, reusing the existing session-briefing machinery, price provider, and saved-report directory.
  • Multi-chat auth via TELEGRAM_ALLOWED_CHAT_IDS so a bot shared between multiple users can authorise each chat while still replying to the sender.
  • Per-chat sliding-window rate limiting (default 20 commands / 60 s) so a runaway loop or abusive chat can't hammer the bot.

What changed

  • qracer/notifications/telegram_poller.py

    • BotCommand gains a chat_id field — the sender's chat — so replies can target the right chat when more than one is authorised.
    • TelegramBotPoller.__init__ accepts allowed_chat_ids: list[str] | None. The primary chat_id is always authorised; extras are deduped and blanks are dropped. Exposed via allowed_chat_ids property.
    • New _RateBucket (sliding-window deque[float] per chat) gated by rate_limit_commands / rate_limit_window_seconds. Over-limit commands are logged and dropped from poll(), but the update offset still advances so we never re-fetch them. rate_limit_commands=0 disables 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.py

    • build_telegram_poller reads TELEGRAM_ALLOWED_CHAT_IDS (comma-separated), parses out blanks, threads it into the poller, and logs (authorised chats: N) when N > 1.
  • qracer/server.py

    • Server.__init__ accepts watchlist, data_registry, sessions_dir, reports_dir.
    • _dispatch_bot_command is now async — handlers returning dynamic data can await data-registry / briefing calls.
    • _cmd_briefing delegates to qracer.conversation.quickpath.generate_briefing. Returns a graceful hint when deps are missing, when there's no prior session, or when the helper raises.
    • _cmd_watchlist fetches prices via PriceProvider, tolerates per-ticker feed failures ("X: price unavailable"), and gracefully degrades to a ticker-only list when no data_registry is configured.
    • _cmd_thesis scans reports_dir for .md files containing a ## Trade Thesis section (newest first), takes up to three, and extracts the thesis body via a new _extract_thesis_section helper (capped at 800 chars, truncated at the next ## / --- boundary).
    • _handle_bot_command now forwards command.chat_id into send_reply, so replies go back to whichever authorised chat issued the command.
  • qracer/cli.py::serve

    • Builds a shared Watchlist unconditionally (previously only when autonomous_enabled).
    • Wires watchlist, data_registry, sessions_dir=_user_dir()/"sessions", reports_dir=_user_dir()/"reports" into Server(...).
    • Startup banner prints the authorised-chat count when multiple chats are configured.

Scope mapping (#165)

Scope item Status
Wire TelegramBotPoller into qracer serve event loop ✅ already in place on main
Route incoming Telegram messages to ConversationEngine.query() ⚠ deferred — needs ConversationEngine (plus session logger / memory searcher / summaries dir) wired into serve; sized as a follow-up
Autonomous monitoring alerts (#161) via Telegram ✅ already flows through NotificationRegistry on main
/briefing Telegram command ✅ new
/watchlist Telegram command ✅ new
/thesis Telegram command ✅ new (reads reports_dir; will be upgraded to FactStore-backed once #157/#158 land)
telegram.bot_token config ✅ already in place (TELEGRAM_BOT_TOKEN credential)
telegram.allowed_chat_ids config ✅ new TELEGRAM_ALLOWED_CHAT_IDS credential
Rate limiting + error handling ✅ per-chat sliding-window + existing try/except around poll/dispatch

The 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 the serve command, which is out of scope for a focused bot-commands PR.

Test plan

  • uv run pytest801 passed, 14 skipped (31 new tests).
  • uv run ruff check on 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_ids defaults 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 rejects invalid kwargs, drops over-limit commands while advancing the offset, buckets are per-chat, expired window allows a new admission, rate_limit_commands=0 blocks everything.
  • BotCommand.parse records the supplied chat id and coerces to str.

tests/notifications/test_factory.py (8 new)

  • build_telegram_poller returns None without credentials, parses a comma-separated TELEGRAM_ALLOWED_CHAT_IDS, trims blanks, dedupes primary, keeps primary when only the primary is configured.

tests/test_server.py (14 new)

  • /briefing handler: 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 leaking Data Sources, capping at three entries.
  • Chat-routed replies: command.chat_id is passed through to send_reply; a blank chat_id becomes None so the poller's primary is used.

Manual verification

from qracer.notifications.factory import build_telegram_poller

poller = build_telegram_poller(
    {
        "TELEGRAM_BOT_TOKEN": "tok",
        "TELEGRAM_CHAT_ID": "111",
        "TELEGRAM_ALLOWED_CHAT_IDS": "222, 333",
    }
)
assert poller.allowed_chat_ids == ("111", "222", "333")

https://claude.ai/code/session_01M88YFVX5Ez393LjQLPNLzH

claude added 2 commits April 15, 2026 08:19
…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
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.

2 participants