diff --git a/README.md b/README.md index ae363c4..08b6992 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ Each is off by default. The holding-news DM and the movers reply are switched on |---|---|---| | `/start` | DM | Greeting (and the buttons if enabled) | | `/holdingnews` | DM, if holding news is on | Run the holding-news check now | -| `/credits` | `OWNER_USER_ID` | Provider balance (OpenRouter; xAI and z.ai have none to show) | -| `/usage` | `OWNER_USER_ID` | Requests, tokens (and % cached) and cost per model, last 24 h and 7 days | +| `/credits` | a user ID in `ADMIN_CHAT_IDS` | Provider balance (OpenRouter; xAI and z.ai have none to show) | +| `/usage` | a user ID in `ADMIN_CHAT_IDS` | Requests, tokens (and % cached) and cost per model, last 24 h and 7 days | ## Caching and token saving **Provider prompt caching** (xAI and OpenRouter keep a conversation on the server holding its cached @@ -209,6 +209,7 @@ tokens, cached % and cost. Tool servers log their tools in short wrapped lines; token size of the tool definitions. The `Starting ...` line shows the git commit the checkout is on. ## Environment variables +### Common settings | Variable | Meaning | |---|---| | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather (required). | @@ -219,21 +220,24 @@ token size of the tool definitions. The `Starting ...` line shows the git commit | `FAST_MODEL` | Optional cheaper/faster model used for simple requests (TOKEN_SAVER only). | | `REASONING` | Reasoning effort: low, medium or high; empty = the model's default (low on z.ai, which cannot turn thinking off). | | `SEARCH` | on|off. Web search (and X search on xAI; z.ai's search API on z.ai). Default on. | +| `ADMIN_CHAT_IDS` | Telegram IDs of the bot's admins (comma/space separated). Your user ID gets a message when an MCP server goes down and may use `/credits` and `/usage`; a group's chat ID gets the down-server messages only. Empty (default) = no alerts and no admin commands. | + +### Advanced and optional +| Variable | Meaning | +|---|---| | `SEARCH_MODEL` | OpenRouter only: model that runs the searches (default xiaomi/mimo-v2.6-flash:online). | | `MAX_TOKENS` | Reply cap in tokens, reasoning included (default 3000). | +| `TOKEN_SAVER` | on|off. Master switch for the token-saving heuristics (default on). | | `HISTORY_LIMIT` | Messages of chat history in the prompt; the window is HISTORY_LIMIT to 1.5x (default 20). | | `DB_PATH` | SQLite file for chat history (default chat_log.db). Instances may share it. | | `BOT_TZ` | Time zone for timestamps, e.g. Australia/Melbourne (default UTC). | | `MCP_CONFIG` | MCP server config file (default mcp_servers.json; missing = no MCP tools). | -| `ADMIN_CHAT_IDS` | Chat IDs told when an MCP server goes down (comma/space separated; empty = no alerts). | -| `OWNER_USER_ID` | Telegram user ID allowed to use /credits and /usage. | | `MOVERS_BOTS` | Usernames of bots whose end-of-day big-movers lists get explained. Empty (default) = feature off. | | `POST_TO_GROUPS_FROM_DM` | on|off. A group admin can have the bot post in that group from a DM (default off). | | `TELEGRAM_DM_BUTTONS` | on|off. Preset-prompt buttons in private chats (default off). | -| `SHARESIGHT_HOLDING_NEWS_TIME` | HH:MM (BOT_TZ) for the daily holding-news check (default 08:00). | | `SHARESIGHT_HOLDING_NEWS_RECIPIENTS` | Comma list of portfolio:telegram_username pairs to notify. Empty (default) = daily holding-news DM off. | +| `SHARESIGHT_HOLDING_NEWS_TIME` | HH:MM (BOT_TZ) for the daily holding-news check (default 08:00). | | `PORTFOLIO_NAMES` | Comma list of Sharesight portfolio names; a message naming one is treated as a portfolio question (default: the recipients' portfolio names). | -| `TOKEN_SAVER` | on|off. Master switch for the token-saving heuristics (default on). | ## Choosing a model Figures from search results and Artificial Analysis at the time of writing (October 2026), so diff --git a/lib/app.py b/lib/app.py index 911d455..9cb6b3c 100644 --- a/lib/app.py +++ b/lib/app.py @@ -123,7 +123,7 @@ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: return wrapper # Commands first: the first matching handler wins, and the catch-all would swallow them. - if st.owner_id: + if st.admin_chats: app.add_handler(CommandHandler("credits", command(commands.credits))) app.add_handler(CommandHandler("usage", command(commands.usage))) app.add_handler(MessageHandler( diff --git a/lib/config.py b/lib/config.py index d5a324e..4f72a76 100644 --- a/lib/config.py +++ b/lib/config.py @@ -11,6 +11,7 @@ # Every environment variable the bot reads, with a one-line description. README.md must # mention each name (tests/test_config.py enforces it). ENV_VARS: dict[str, str] = { + # Common "TELEGRAM_BOT_TOKEN": "Bot token from @BotFather (required).", "XAI_API_KEY": "xAI key; selects the xAI provider. Set exactly one of the three keys.", "OPENROUTER_API_KEY": "OpenRouter key; selects the OpenRouter provider.", @@ -19,21 +20,21 @@ "FAST_MODEL": "Optional cheaper/faster model used for simple requests (TOKEN_SAVER only).", "REASONING": "Reasoning effort: low, medium or high; empty = the model's default (low on z.ai, which cannot turn thinking off).", "SEARCH": "on|off. Web search (and X search on xAI; z.ai's search API on z.ai). Default on.", + "ADMIN_CHAT_IDS": "Telegram IDs of the bot's admins (comma/space separated). Your user ID gets MCP-down alerts and may use /credits and /usage; a group's chat ID gets the alerts only. Empty = neither.", + # Advanced and optional "SEARCH_MODEL": "OpenRouter only: model that runs the searches (default xiaomi/mimo-v2.6-flash:online).", "MAX_TOKENS": "Reply cap in tokens, reasoning included (default 3000).", + "TOKEN_SAVER": "on|off. Master switch for the token-saving heuristics (default on).", "HISTORY_LIMIT": "Messages of chat history in the prompt; the window is HISTORY_LIMIT to 1.5x (default 20).", "DB_PATH": "SQLite file for chat history (default chat_log.db). Instances may share it.", "BOT_TZ": "Time zone for timestamps, e.g. Australia/Melbourne (default UTC).", "MCP_CONFIG": "MCP server config file (default mcp_servers.json; missing = no MCP tools).", - "ADMIN_CHAT_IDS": "Chat IDs told when an MCP server goes down (comma/space separated; empty = no alerts).", - "OWNER_USER_ID": "Telegram user ID allowed to use /credits and /usage.", "MOVERS_BOTS": "Usernames of bots whose end-of-day big-movers lists get explained. Empty (default) = feature off.", "POST_TO_GROUPS_FROM_DM": "on|off. Lets an admin of a group the bot is in make it post there from a DM (default off).", "TELEGRAM_DM_BUTTONS": "on|off. Preset-prompt buttons in private chats (default off).", - "SHARESIGHT_HOLDING_NEWS_TIME": "HH:MM (BOT_TZ) for the daily holding-news check (default 08:00).", "SHARESIGHT_HOLDING_NEWS_RECIPIENTS": "Comma list of portfolio:telegram_username pairs to notify. Empty (default) = daily holding-news DM off.", + "SHARESIGHT_HOLDING_NEWS_TIME": "HH:MM (BOT_TZ) for the daily holding-news check (default 08:00).", "PORTFOLIO_NAMES": "Comma list of Sharesight portfolio names; a message naming one is treated as a portfolio question (default: the recipients' portfolio names).", - "TOKEN_SAVER": "on|off. Master switch for the token-saving heuristics (default on).", } DEFAULT_MODELS = {"xai": "grok-4.3", "openrouter": "z-ai/glm-5.3-flash", "zai": "glm-5.3-flash"} @@ -70,8 +71,7 @@ class Settings: db_path: str tz: ZoneInfo mcp_config: str - admin_chats: frozenset[int] - owner_id: int + admin_chats: frozenset[int] # user IDs (alerts + admin commands) and group chat IDs (alerts only) movers_bots: frozenset[str] dm_buttons: bool post_to_groups: bool @@ -165,7 +165,6 @@ def load(env: Mapping[str, str] | None = None) -> Settings: tz=ZoneInfo(env.get("BOT_TZ", "UTC")), mcp_config=env.get("MCP_CONFIG", "mcp_servers.json"), admin_chats=_ids(env.get("ADMIN_CHAT_IDS", "")), - owner_id=int(env.get("OWNER_USER_ID") or 0), movers_bots=frozenset( u.lower().lstrip("@") for u in env.get("MOVERS_BOTS", "").replace(",", " ").split() ), diff --git a/lib/telegram/commands.py b/lib/telegram/commands.py index 8e53e9b..fc17218 100644 --- a/lib/telegram/commands.py +++ b/lib/telegram/commands.py @@ -16,10 +16,10 @@ async def _gate(ctx: Ctx, update: Update) -> bool: """Log the command message (these handlers take it before on_message would) and say - whether the sender is the owner.""" + whether the sender is an admin (a user ID in ADMIN_CHAT_IDS).""" await asyncio.to_thread(ctx.store.save, update.effective_message) user = update.effective_user - return bool(user and ctx.st.owner_id and user.id == ctx.st.owner_id) + return bool(user and user.id in ctx.st.admin_chats) async def _reply(ctx: Ctx, update: Update, text: str) -> None: @@ -28,7 +28,7 @@ async def _reply(ctx: Ctx, update: Update, text: str) -> None: async def credits(ctx: Ctx, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """/credits - owner only: how much provider credit is left (OpenRouter).""" + """/credits - admins only: how much provider credit is left (OpenRouter).""" if not await _gate(ctx, update): return try: @@ -51,7 +51,7 @@ def usage_text(rows: list[tuple], label: str) -> str: async def usage(ctx: Ctx, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """/usage - owner only: tokens and cost per model for the last 24 hours and 7 days.""" + """/usage - admins only: tokens and cost per model for the last 24 hours and 7 days.""" if not await _gate(ctx, update): return now = int(time.time()) diff --git a/tests/test_app.py b/tests/test_app.py index f8355f3..df766fe 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -31,7 +31,7 @@ def build(**extra): plain, st = build() assert handler_kinds(plain) == ["MessageHandler"] and app.allowed_updates(st) == ["message", "edited_message"] - full, st = build(OWNER_USER_ID="5", SHARESIGHT_HOLDING_NEWS_RECIPIENTS="Pf:alice") + full, st = build(ADMIN_CHAT_IDS="5", SHARESIGHT_HOLDING_NEWS_RECIPIENTS="Pf:alice") kinds = handler_kinds(full) assert kinds == ["CommandHandler", "CommandHandler", "MessageHandler", "CallbackQueryHandler"] assert "callback_query" in app.allowed_updates(st) diff --git a/tests/test_features.py b/tests/test_features.py index 50036d4..eb91a04 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -165,8 +165,8 @@ def test_movers_helpers(): # ---- commands -------------------------------------------------------------------------- -async def test_credits_and_usage_are_owner_only_and_logged(env, store): - st = config.load({**env, "OWNER_USER_ID": "5"}) +async def test_credits_and_usage_are_admin_only_and_logged(env, store): + st = config.load({**env, "ADMIN_CHAT_IDS": "5, -100"}) ctx = Ctx(st, store, ScriptedBackend([]), registry(), FakeBot()) store.add_usage(-1, "m", "chat", 1, 1000, 500, 20, 0.5) stranger = user_msg(ctx.bot, "/usage", user_id=9, mid=1)