diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index bb155b8..02693d8 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -338,6 +338,8 @@ "chatName": "Name", "chatType": "Type", "chatLang": "Lang", + "cmds": "Cmds", + "commandsToggle": "Toggle command listening for this chat", "chatId": "Chat ID", "languageUpdated": "Chat language updated", "cmdLocale": "Bot language", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index a187763..6ce7591 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -338,6 +338,8 @@ "chatName": "Имя", "chatType": "Тип", "chatLang": "Язык", + "cmds": "Команды", + "commandsToggle": "Включить/выключить команды для этого чата", "chatId": "ID чата", "languageUpdated": "Язык чата обновлён", "cmdLocale": "Язык бота", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 37294c2..00e6285 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -52,6 +52,7 @@ export interface TelegramChat { type: string; username: string; language_code?: string; + commands_enabled: boolean; discovered_at: string; } diff --git a/frontend/src/routes/bots/TelegramBotTab.svelte b/frontend/src/routes/bots/TelegramBotTab.svelte index 46a66d2..97da4ed 100644 --- a/frontend/src/routes/bots/TelegramBotTab.svelte +++ b/frontend/src/routes/bots/TelegramBotTab.svelte @@ -131,7 +131,6 @@ method: 'PUT', body: JSON.stringify({ language_code: lang }), }); - // Update local state immutably chats[botId] = (chats[botId] || []).map(c => c.id === chat.id ? { ...c, language_code: lang } : c ); @@ -139,6 +138,19 @@ } catch (err: any) { snackError(err.message); } } + async function toggleChatCommands(botId: number, chat: TelegramChat) { + const newVal = !chat.commands_enabled; + try { + await api(`/telegram-bots/${botId}/chats/${chat.id}`, { + method: 'PUT', + body: JSON.stringify({ commands_enabled: newVal }), + }); + chats[botId] = (chats[botId] || []).map(c => + c.id === chat.id ? { ...c, commands_enabled: newVal } : c + ); + } catch (err: any) { snackError(err.message); } + } + async function loadListenerStatus(botId: number) { botListenerLoading = { ...botListenerLoading, [botId]: true }; try { @@ -156,6 +168,19 @@ botListenerLoading = { ...botListenerLoading, [botId]: false }; } + async function toggleListenerEnabled(botId: number, trk: CommandTrackerSummary) { + const endpoint = trk.enabled ? 'disable' : 'enable'; + try { + await api(`/command-trackers/${trk.id}/${endpoint}`, { method: 'POST' }); + botListenerStatus = { + ...botListenerStatus, + [botId]: (botListenerStatus[botId] || []).map(t => + t.id === trk.id ? { ...t, enabled: !t.enabled } : t + ), + }; + } catch (err: any) { snackError(err.message); } + } + async function syncCommands(botId: number) { modeChanging = { ...modeChanging, [botId]: true }; try { @@ -337,13 +362,14 @@ {:else if (chats[bot.id] || []).length === 0}

{t('telegramBot.noChats')}

{:else} - {@const gridStyle = "display:grid; grid-template-columns:1fr 80px 100px 130px 60px; align-items:center; gap:0.5rem;"} + {@const gridStyle = "display:grid; grid-template-columns:1fr 80px 100px 50px 130px 60px; align-items:center; gap:0.5rem;"}
{t('telegramBot.chatName')} {t('telegramBot.chatType')} {t('telegramBot.chatLang')} + {t('telegramBot.cmds')} {t('telegramBot.chatId')}
@@ -364,6 +390,14 @@ onselect={(val) => updateChatLanguage(bot.id, chat, String(val ?? ''))} /> +
e.stopPropagation()}> + +
{chat.chat_id}
- {trk.name} - - {trk.enabled ? t('commandTracker.enabled') : t('commandTracker.disabled')} - + {trk.name}
- - {t('common.edit')} - +
{/each} diff --git a/packages/server/src/notify_bridge_server/api/command_trackers.py b/packages/server/src/notify_bridge_server/api/command_trackers.py index e73ab33..6891af0 100644 --- a/packages/server/src/notify_bridge_server/api/command_trackers.py +++ b/packages/server/src/notify_bridge_server/api/command_trackers.py @@ -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(), } diff --git a/packages/server/src/notify_bridge_server/api/telegram_bots.py b/packages/server/src/notify_bridge_server/api/telegram_bots.py index 26ecf0f..c3c6fba 100644 --- a/packages/server/src/notify_bridge_server/api/telegram_bots.py +++ b/packages/server/src/notify_bridge_server/api/telegram_bots.py @@ -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(), } diff --git a/packages/server/src/notify_bridge_server/commands/webhook.py b/packages/server/src/notify_bridge_server/commands/webhook.py index 66f2f62..153f9a7 100644 --- a/packages/server/src/notify_bridge_server/commands/webhook.py +++ b/packages/server/src/notify_bridge_server/commands/webhook.py @@ -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: diff --git a/packages/server/src/notify_bridge_server/database/migrations.py b/packages/server/src/notify_bridge_server/database/migrations.py index cb9ab22..6cf6dbd 100644 --- a/packages/server/src/notify_bridge_server/database/migrations.py +++ b/packages/server/src/notify_bridge_server/database/migrations.py @@ -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) diff --git a/packages/server/src/notify_bridge_server/database/models.py b/packages/server/src/notify_bridge_server/database/models.py index 76d7a2a..cc26e79 100644 --- a/packages/server/src/notify_bridge_server/database/models.py +++ b/packages/server/src/notify_bridge_server/database/models.py @@ -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) diff --git a/packages/server/src/notify_bridge_server/services/telegram_poller.py b/packages/server/src/notify_bridge_server/services/telegram_poller.py index d0ea1a4..2fbf1b2 100644 --- a/packages/server/src/notify_bridge_server/services/telegram_poller.py +++ b/packages/server/src/notify_bridge_server/services/telegram_poller.py @@ -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: