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:
+37
-2
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user