feat: add group/channel support for Telegram bot

- Add TELEGRAM_GROUP_IDS configuration setting to enable optional group/channel filtering
- Add _is_group_enabled() authorization check to all command handlers
- Add group/channel authorization check to message_handler
- All handlers now verify both user authorization and group authorization
- If TELEGRAM_GROUP_IDS is not configured, bot accepts messages from any group
- If TELEGRAM_GROUP_IDS is configured, bot only processes messages from those groups

This enables the bot to be used in group chats and channels with Topics/Threads support.
This commit is contained in:
Daniel Wagner
2026-07-26 12:26:57 +10:00
parent a3ca696546
commit 669b57a317
3 changed files with 42 additions and 6 deletions
+37 -2
View File
@@ -75,6 +75,13 @@ def _is_allowed(user_id: int, settings: Settings) -> bool:
return user_id in settings.telegram_allowed_user_ids
def _is_group_enabled(chat_id: int, settings: Settings) -> bool:
"""Return True if the group/channel is in the configured list (or no list is configured)."""
if not settings.telegram_group_ids:
return True
return chat_id in settings.telegram_group_ids
async def _send_long(update: Update, text: str) -> None:
"""Send text, splitting across messages if it exceeds Telegram's 4096-char limit."""
limit = 4096
@@ -92,6 +99,10 @@ async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
await update.message.reply_text( # type: ignore[union-attr]
"Hello, I'm *Steward* \U0001f916\n\n"
"I'm your AI-assisted personal operations platform.\n"
@@ -112,6 +123,10 @@ async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
await update.message.reply_text( # type: ignore[union-attr]
"*Steward commands*\n\n"
"/start – greeting\n"
@@ -136,6 +151,10 @@ async def clear_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
key = _thread_key(update)
if key is not None:
_thread_history[key].clear()
@@ -167,6 +186,10 @@ async def flush_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
key = _thread_key(update)
if key is None:
await update.message.reply_text( # type: ignore[union-attr]
@@ -246,6 +269,10 @@ async def recall_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
# If the user supplied a keyword query, search the knowledge base
args: list[str] = context.args or [] # type: ignore[assignment]
if args:
@@ -306,13 +333,16 @@ async def analyse_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
if user is None or not _is_allowed(user.id, settings):
return
chat = update.effective_chat
if chat is None or not _is_group_enabled(chat.id, settings):
return
await update.message.reply_text("Running analysis, please wait...") # type: ignore[union-attr]
generator = ProposalGenerator(settings, llm)
proposal = await generator.run()
if proposal is None:
await update.message.reply_text( # type: ignore[union-attr]
"Analysis could not be completed. "
"Check that `ANALYSIS_TARGET_URL` is configured."
"Analysis could not be completed. Check that `ANALYSIS_TARGET_URL` is configured."
)
return
@@ -356,8 +386,11 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
store: ThreadMemoryStore = context.bot_data["thread_store"]
tool_client: ToolClient | None = context.bot_data.get("tool_client")
user = update.effective_user
chat = update.effective_chat
if user is None or not _is_allowed(user.id, settings):
return
if chat is None or not _is_group_enabled(chat.id, settings):
return
text = update.message.text # type: ignore[union-attr]
if not text:
@@ -402,6 +435,8 @@ def build_application(
app.bot_data["llm"] = llm
app.bot_data["thread_store"] = thread_store or ThreadMemoryStore(settings.thread_memory_path)
app.bot_data["tool_client"] = tool_client # None when tools are not configured
logger.info("Configured allowed users: %s", settings.telegram_allowed_user_ids)
logger.info("Configured group IDs: %s", settings.telegram_group_ids)
app.add_handler(CommandHandler("start", start_handler))
app.add_handler(CommandHandler("help", help_handler))