diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json
index ba41a39..ebd130e 100644
--- a/frontend/src/lib/i18n/en.json
+++ b/frontend/src/lib/i18n/en.json
@@ -433,6 +433,8 @@
"webhookRegistered": "Webhook registered",
"webhookUnregistered": "Webhook unregistered",
"updateMode": "Update mode",
+ "none": "None",
+ "noneActive": "Listener disabled",
"polling": "Polling",
"webhook": "Webhook",
"webhookStatus": "Webhook status",
diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json
index ee439a0..a0d96e5 100644
--- a/frontend/src/lib/i18n/ru.json
+++ b/frontend/src/lib/i18n/ru.json
@@ -433,6 +433,8 @@
"webhookRegistered": "Вебхук зарегистрирован",
"webhookUnregistered": "Вебхук удалён",
"updateMode": "Режим обновлений",
+ "none": "Откл.",
+ "noneActive": "Приём обновлений отключён",
"polling": "Опрос",
"webhook": "Вебхук",
"webhookStatus": "Статус вебхука",
diff --git a/frontend/src/routes/bots/TelegramBotTab.svelte b/frontend/src/routes/bots/TelegramBotTab.svelte
index 2d14a33..5aabe50 100644
--- a/frontend/src/routes/bots/TelegramBotTab.svelte
+++ b/frontend/src/routes/bots/TelegramBotTab.svelte
@@ -334,10 +334,12 @@
@{bot.bot_username}
{/if}
-
- {bot.update_mode === 'webhook' ? t('telegramBot.webhook') : t('telegramBot.polling')}
+ : (bot.update_mode || 'none') === 'polling'
+ ? 'bg-[var(--color-success-bg)] text-[var(--color-success-fg)]'
+ : 'bg-[var(--color-muted)] text-[var(--color-muted-foreground)]'}">
+ {(bot.update_mode || 'none') === 'webhook' ? t('telegramBot.webhook') : (bot.update_mode || 'none') === 'polling' ? t('telegramBot.polling') : t('telegramBot.none')}
{bot.token_preview}
@@ -456,6 +458,14 @@
{t('telegramBot.updateMode')}
+
+ {#if (bot.update_mode || 'none') === 'none'}
+
+
+ {t('telegramBot.noneActive')}
+
+ {/if}
+
{#if bot.update_mode === 'polling'}
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 f0370c4..d74e1e9 100644
--- a/packages/server/src/notify_bridge_server/api/telegram_bots.py
+++ b/packages/server/src/notify_bridge_server/api/telegram_bots.py
@@ -86,6 +86,11 @@ async def update_bot(
bot.icon = body.icon
# Handle mode switching
if body.update_mode is not None and body.update_mode != bot.update_mode:
+ if body.update_mode not in ("none", "polling", "webhook"):
+ raise HTTPException(
+ status_code=400,
+ detail=f"Invalid update_mode: {body.update_mode!r}. Must be 'none', 'polling', or 'webhook'.",
+ )
if body.update_mode == "webhook":
# Validate and register webhook BEFORE stopping polling
base_url = await get_setting(session, "external_url")
@@ -108,6 +113,12 @@ async def update_bot(
# Switching to polling: unregister webhook, start polling
await unregister_webhook(bot.token)
schedule_bot_polling(bot.id)
+ elif body.update_mode == "none":
+ # Disable listener: stop polling and clear any webhook so Telegram
+ # stops delivering updates. This makes the bot send-only, which is
+ # safe when another instance owns the listener.
+ unschedule_bot_polling(bot.id)
+ await unregister_webhook(bot.token)
bot.update_mode = body.update_mode
session.add(bot)
@@ -406,7 +417,7 @@ def _bot_response(b: TelegramBot) -> dict:
"bot_username": b.bot_username,
"bot_id": b.bot_id,
"webhook_path_id": b.webhook_path_id,
- "update_mode": b.update_mode or "polling",
+ "update_mode": b.update_mode or "none",
"token_preview": f"{b.token[:8]}...{b.token[-4:]}" if len(b.token) > 12 else "***",
"created_at": b.created_at.isoformat(),
}
diff --git a/packages/server/src/notify_bridge_server/database/migrations.py b/packages/server/src/notify_bridge_server/database/migrations.py
index 84dbc50..884270c 100644
--- a/packages/server/src/notify_bridge_server/database/migrations.py
+++ b/packages/server/src/notify_bridge_server/database/migrations.py
@@ -144,7 +144,10 @@ async def migrate_schema(engine: AsyncEngine) -> None:
if bots:
logger.info("Backfilled webhook_path_id for %d existing bots", len(bots))
- # Add update_mode to telegram_bot if missing
+ # Add update_mode to telegram_bot if missing.
+ # Existing bots pre-date this feature and were implicitly polling;
+ # preserve that behavior. New bots default to "none" via the
+ # SQLModel field default on fresh schemas.
if not await _has_column(conn, "telegram_bot", "update_mode"):
await conn.execute(
text("ALTER TABLE telegram_bot ADD COLUMN update_mode TEXT DEFAULT 'polling'")
diff --git a/packages/server/src/notify_bridge_server/database/models.py b/packages/server/src/notify_bridge_server/database/models.py
index 6f6a778..071733f 100644
--- a/packages/server/src/notify_bridge_server/database/models.py
+++ b/packages/server/src/notify_bridge_server/database/models.py
@@ -49,7 +49,7 @@ class TelegramBot(SQLModel, table=True):
bot_username: str = Field(default="")
bot_id: int = Field(default=0)
webhook_path_id: str = Field(default_factory=lambda: uuid4().hex)
- update_mode: str = Field(default="polling") # "polling" or "webhook"
+ update_mode: str = Field(default="none") # "none", "polling", or "webhook"
# NOTE: commands_config column remains in the DB for backward compat,
# but is no longer part of the SQLModel class. Data migrated to CommandConfig.
created_at: datetime = Field(default_factory=_utcnow)
diff --git a/packages/server/src/notify_bridge_server/services/backup_schema.py b/packages/server/src/notify_bridge_server/services/backup_schema.py
index e3443e8..01ded51 100644
--- a/packages/server/src/notify_bridge_server/services/backup_schema.py
+++ b/packages/server/src/notify_bridge_server/services/backup_schema.py
@@ -173,7 +173,7 @@ class TelegramBotData(BaseModel):
token: str = ""
icon: str = ""
bot_username: str = ""
- update_mode: str = "polling"
+ update_mode: str = "none"
class MatrixBotData(BaseModel):