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: