feat: per-chat command toggle, listener name + toggle in bot tab

- Add commands_enabled field to TelegramChat (default off) with
  migration, gating command dispatch in both poller and webhook
- Show toggle switch per chat in bot tab for enabling/disabling commands
- Fix listener response to include bot name instead of just type
- Replace listener "Enabled" label + "Edit" link with toggle switch
  and crosslink to command-trackers page
This commit is contained in:
2026-03-23 19:23:37 +03:00
parent 37388c430c
commit b3b6c31c4d
10 changed files with 90 additions and 24 deletions
@@ -256,7 +256,7 @@ async def list_listeners(
CommandTrackerListener.command_tracker_id == tracker_id
)
)
return [_listener_response(l) for l in result.all()]
return [await _listener_response(session, l) for l in result.all()]
@router.post("/{tracker_id}/listeners", status_code=status.HTTP_201_CREATED)
@@ -312,7 +312,7 @@ async def add_listener(
from ..services.command_sync import mark_bot_dirty
mark_bot_dirty(body.listener_id)
return _listener_response(listener)
return await _listener_response(session, listener)
@router.delete("/{tracker_id}/listeners/{listener_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -377,17 +377,23 @@ async def _tracker_response(
CommandTrackerListener.command_tracker_id == t.id
)
)
resp["listeners"] = [_listener_response(l) for l in lr.all()]
resp["listeners"] = [await _listener_response(session, l) for l in lr.all()]
return resp
def _listener_response(l: CommandTrackerListener) -> dict:
async def _listener_response(session: AsyncSession, l: CommandTrackerListener) -> dict:
name = ""
if l.listener_type == "telegram_bot":
bot = await session.get(TelegramBot, l.listener_id)
if bot:
name = bot.name
return {
"id": l.id,
"command_tracker_id": l.command_tracker_id,
"listener_type": l.listener_type,
"listener_id": l.listener_id,
"name": name,
"created_at": l.created_at.isoformat(),
}
@@ -299,6 +299,7 @@ async def test_chat(
class ChatUpdate(BaseModel):
language_code: str | None = None
title: str | None = None
commands_enabled: bool | None = None
@router.put("/{bot_id}/chats/{chat_db_id}")
@@ -320,11 +321,7 @@ async def update_chat(
session.add(chat)
await session.commit()
await session.refresh(chat)
return {
"id": chat.id, "bot_id": chat.bot_id, "chat_id": chat.chat_id,
"title": chat.title, "type": chat.chat_type, "username": chat.username,
"language_code": chat.language_code, "discovered_at": chat.discovered_at.isoformat() if chat.discovered_at else None,
}
return _chat_response(chat)
@router.delete("/{bot_id}/chats/{chat_db_id}", status_code=status.HTTP_204_NO_CONTENT)
@@ -392,6 +389,7 @@ def _chat_response(c: TelegramChat) -> dict:
"type": c.chat_type,
"username": c.username,
"language_code": getattr(c, 'language_code', '') or '',
"commands_enabled": getattr(c, 'commands_enabled', False),
"discovered_at": c.discovered_at.isoformat(),
}
@@ -13,7 +13,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from notify_bridge_core.notifications.telegram.client import TelegramClient
from ..database.engine import get_session
from ..database.models import TelegramBot
from ..database.models import TelegramBot, TelegramChat
from ..services.telegram import save_chat_from_webhook
from .handler import handle_command, send_media_group, send_reply
@@ -76,8 +76,16 @@ async def telegram_webhook(
except Exception:
_LOGGER.warning("Failed to auto-save chat %s", chat_id, exc_info=True)
# Handle commands
# Handle commands (only if chat has commands enabled)
if text.startswith("/"):
chat_row = (await session.exec(
select(TelegramChat).where(
TelegramChat.bot_id == bot.id,
TelegramChat.chat_id == chat_id,
)
)).first()
if not chat_row or not chat_row.commands_enabled:
return {"ok": True, "skipped": "commands_disabled"}
message_id = message.get("message_id")
cmd_response = await handle_command(bot, chat_id, text, language_code=msg_language)
if cmd_response is not None:
@@ -207,6 +207,13 @@ async def migrate_schema(engine: AsyncEngine) -> None:
)
logger.info("Added language_code column to telegram_chat table")
# Add commands_enabled to telegram_chat if missing (default disabled)
if not await _has_column(conn, "telegram_chat", "commands_enabled"):
await conn.execute(
text("ALTER TABLE telegram_chat ADD COLUMN commands_enabled INTEGER DEFAULT 0")
)
logger.info("Added commands_enabled column to telegram_chat table")
# ---------------------------------------------------------------------------
# Legacy tracker_target migration (pre-Phase 1)
@@ -96,6 +96,7 @@ class TelegramChat(SQLModel, table=True):
chat_type: str = Field(default="private")
username: str = Field(default="")
language_code: str = Field(default="")
commands_enabled: bool = Field(default=False)
discovered_at: datetime = Field(default_factory=_utcnow)
@@ -20,7 +20,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from notify_bridge_core.notifications.telegram.client import TelegramClient
from ..database.engine import get_engine
from ..database.models import CommandTracker, CommandTrackerListener, TelegramBot
from ..database.models import CommandTracker, CommandTrackerListener, TelegramBot, TelegramChat
from ..services.telegram import save_chat_from_webhook
from .scheduler import get_scheduler
@@ -195,9 +195,18 @@ async def _poll_bot(bot_id: int) -> None:
except Exception:
_LOGGER.debug("Failed to auto-save chat %s", chat_id, exc_info=True)
# Dispatch commands
# Dispatch commands (only if chat has commands enabled)
if text and text.startswith("/"):
try:
async with AsyncSession(engine) as cmd_session:
chat_row = (await cmd_session.exec(
select(TelegramChat).where(
TelegramChat.bot_id == bot_obj.id,
TelegramChat.chat_id == chat_id,
)
)).first()
if not chat_row or not chat_row.commands_enabled:
continue
message_id = message.get("message_id")
cmd_response = await handle_command(bot_obj, chat_id, text, language_code=msg_language)
if cmd_response is not None: