feat(telegram): add 'none' listener mode for bots

Introduce a third update_mode option alongside polling/webhook. 'none'
disables both polling and webhook delivery — useful when another instance
owns the listener or when the bot is send-only. Switching into 'none' now
unschedules polling and unregisters any active webhook so Telegram stops
delivering updates.

New bots default to 'none' (safer when multiple bridges share a token).
Existing bots upgraded from a pre-update_mode schema keep 'polling' so
their behavior is unchanged.
This commit is contained in:
2026-04-24 15:15:25 +03:00
parent 461fb495d7
commit be15463fd2
7 changed files with 42 additions and 7 deletions
@@ -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(),
}
@@ -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'")
@@ -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)
@@ -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):