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:
parent
a3ca696546
commit
669b57a317
@ -1,10 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
steward:
|
steward:
|
||||||
image: ghcr.io/djw4/steward:latest
|
# image: ghcr.io/djw4/steward:latest
|
||||||
# To build locally instead: uncomment the next line and comment out image above
|
# To build locally instead: uncomment the next line and comment out image above
|
||||||
# build: .
|
build: .
|
||||||
user: "1000:1000" # matches the UID/GID created in the Dockerfile
|
user: "1000:1000" # matches the UID/GID created in the Dockerfile
|
||||||
env_file: .env # copy .env.example → .env and fill in your values
|
env_file: .env # copy .env.example → .env and fill in your values
|
||||||
environment:
|
environment:
|
||||||
THREAD_MEMORY_PATH: /data/thread_memory.json
|
THREAD_MEMORY_PATH: /data/thread_memory.json
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@ -75,6 +75,13 @@ def _is_allowed(user_id: int, settings: Settings) -> bool:
|
|||||||
return user_id in settings.telegram_allowed_user_ids
|
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:
|
async def _send_long(update: Update, text: str) -> None:
|
||||||
"""Send text, splitting across messages if it exceeds Telegram's 4096-char limit."""
|
"""Send text, splitting across messages if it exceeds Telegram's 4096-char limit."""
|
||||||
limit = 4096
|
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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
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]
|
await update.message.reply_text( # type: ignore[union-attr]
|
||||||
"Hello, I'm *Steward* \U0001f916\n\n"
|
"Hello, I'm *Steward* \U0001f916\n\n"
|
||||||
"I'm your AI-assisted personal operations platform.\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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
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]
|
await update.message.reply_text( # type: ignore[union-attr]
|
||||||
"*Steward commands*\n\n"
|
"*Steward commands*\n\n"
|
||||||
"/start – greeting\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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
chat = update.effective_chat
|
||||||
|
if chat is None or not _is_group_enabled(chat.id, settings):
|
||||||
|
return
|
||||||
|
|
||||||
key = _thread_key(update)
|
key = _thread_key(update)
|
||||||
if key is not None:
|
if key is not None:
|
||||||
_thread_history[key].clear()
|
_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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
chat = update.effective_chat
|
||||||
|
if chat is None or not _is_group_enabled(chat.id, settings):
|
||||||
|
return
|
||||||
|
|
||||||
key = _thread_key(update)
|
key = _thread_key(update)
|
||||||
if key is None:
|
if key is None:
|
||||||
await update.message.reply_text( # type: ignore[union-attr]
|
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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
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
|
# If the user supplied a keyword query, search the knowledge base
|
||||||
args: list[str] = context.args or [] # type: ignore[assignment]
|
args: list[str] = context.args or [] # type: ignore[assignment]
|
||||||
if args:
|
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):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
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]
|
await update.message.reply_text("Running analysis, please wait...") # type: ignore[union-attr]
|
||||||
generator = ProposalGenerator(settings, llm)
|
generator = ProposalGenerator(settings, llm)
|
||||||
proposal = await generator.run()
|
proposal = await generator.run()
|
||||||
if proposal is None:
|
if proposal is None:
|
||||||
await update.message.reply_text( # type: ignore[union-attr]
|
await update.message.reply_text( # type: ignore[union-attr]
|
||||||
"Analysis could not be completed. "
|
"Analysis could not be completed. Check that `ANALYSIS_TARGET_URL` is configured."
|
||||||
"Check that `ANALYSIS_TARGET_URL` is configured."
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -356,8 +386,11 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
store: ThreadMemoryStore = context.bot_data["thread_store"]
|
store: ThreadMemoryStore = context.bot_data["thread_store"]
|
||||||
tool_client: ToolClient | None = context.bot_data.get("tool_client")
|
tool_client: ToolClient | None = context.bot_data.get("tool_client")
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
|
chat = update.effective_chat
|
||||||
if user is None or not _is_allowed(user.id, settings):
|
if user is None or not _is_allowed(user.id, settings):
|
||||||
return
|
return
|
||||||
|
if chat is None or not _is_group_enabled(chat.id, settings):
|
||||||
|
return
|
||||||
|
|
||||||
text = update.message.text # type: ignore[union-attr]
|
text = update.message.text # type: ignore[union-attr]
|
||||||
if not text:
|
if not text:
|
||||||
@ -402,6 +435,8 @@ def build_application(
|
|||||||
app.bot_data["llm"] = llm
|
app.bot_data["llm"] = llm
|
||||||
app.bot_data["thread_store"] = thread_store or ThreadMemoryStore(settings.thread_memory_path)
|
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
|
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("start", start_handler))
|
||||||
app.add_handler(CommandHandler("help", help_handler))
|
app.add_handler(CommandHandler("help", help_handler))
|
||||||
|
|||||||
@ -11,6 +11,7 @@ class Settings(BaseSettings):
|
|||||||
# Telegram
|
# Telegram
|
||||||
telegram_bot_token: str = ""
|
telegram_bot_token: str = ""
|
||||||
telegram_allowed_user_ids: list[int] = []
|
telegram_allowed_user_ids: list[int] = []
|
||||||
|
telegram_group_ids: list[int] = []
|
||||||
|
|
||||||
# LLM (OpenAI-compatible)
|
# LLM (OpenAI-compatible)
|
||||||
openai_api_key: str = ""
|
openai_api_key: str = ""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user