Files
default/telegram-agy-bot/bot_actions.py
T

1403 lines
71 KiB
Python

import os
import sys
import re
import json
import time
import shutil
import asyncio
import logging
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple, Callable
from datetime import datetime
from config import settings
from agy_engine import (
session_manager,
AVAILABLE_MODELS,
AVAILABLE_EFFORTS,
AVAILABLE_LANGUAGES,
get_valid_effort_for_model,
Session,
Project,
)
from scheduler import (
task_scheduler,
ScheduledTask,
format_relative_time,
format_timestamp,
)
from backup_manager import backup_manager
from invite_manager import invite_manager
from formatters import markdown_to_telegram_html, escape_html, split_message
logger = logging.getLogger("AGYBotActions")
CADDYFILE_PATH = Path("/etc/caddy/Caddyfile")
WILDCARD_DOMAIN = "msa.artacloud.ir"
# =====================================================================
# Model & Setting Resolvers
# =====================================================================
def resolve_model_name(req_model: str) -> Optional[str]:
"""Resolves exact or fuzzy model nicknames to standard model ID."""
if not req_model:
return None
raw = req_model.lower().strip()
# Exact match
for m in AVAILABLE_MODELS.keys():
if raw == m.lower():
return m
# Fuzzy nicknames
if any(w in raw for w in ("auto", "خودکار", "اتوماتیک", "اتو", "flash-auto", "3.7-auto")):
return "gemini-3.7-flash-auto"
if "opus" in raw:
return "claude-opus-4-6-thinking"
if "sonnet" in raw or "claude" in raw:
return "claude-sonnet-4-6"
if "3.1" in raw or "pro" in raw:
return "gemini-3.1-pro"
if "flash-high" in raw or ("high" in raw and "flash" in raw):
return "gemini-3.7-flash-high"
if "3.6" in raw:
return "gemini-3.6-flash"
if "3.7" in raw or "flash" in raw:
return "gemini-3.7-flash"
if "gpt" in raw or "120b" in raw or "oss" in raw:
return "gpt-oss-120b"
return None
def resolve_effort_level(req_effort: str) -> Optional[str]:
"""Resolves effort level string (low, medium, high)."""
if not req_effort:
return None
raw = req_effort.lower().strip()
if raw in ("low", "medium", "high"):
return raw
if any(w in raw for w in ("high", "زیاد", "بالا", "حداکثر")):
return "high"
if any(w in raw for w in ("med", "medium", "متوسط", "معمولی")):
return "medium"
if any(w in raw for w in ("low", "کم", "پایین", "حداقل")):
return "low"
return None
def resolve_language_code(req_lang: str) -> Optional[str]:
"""Resolves language code from alias or name."""
if not req_lang:
return None
raw = req_lang.lower().strip()
try:
from bot import LANG_ALIASES
raw = LANG_ALIASES.get(raw, raw)
except Exception:
pass
if raw in AVAILABLE_LANGUAGES:
return raw
if any(w in raw for w in ("fa", "farsi", "persian", "فارسی")):
return "fa"
if any(w in raw for w in ("en", "eng", "english", "انگلیسی")):
return "en"
if any(w in raw for w in ("auto", "خودکار", "detect")):
return "auto"
return None
# =====================================================================
# Caddy Reverse Proxy Helpers
# =====================================================================
def configure_caddy_reverse_proxy(
subdomain: str,
port_or_target: str,
custom_domain: Optional[str] = None,
) -> Tuple[bool, str, str]:
"""
Configures Caddyfile to reverse-proxy a subdomain or domain to a local port or target.
Auto-validates and reloads Caddy.
Returns (success, message, full_url).
"""
sub = subdomain.strip().lower()
sub = re.sub(r"[^\w\-.]", "", sub)
if custom_domain:
host = custom_domain.strip().lower()
elif "." in sub:
host = sub
else:
host = f"{sub}.{WILDCARD_DOMAIN}"
target = str(port_or_target).strip()
if target.isdigit():
target = f"localhost:{target}"
elif not target.startswith("http://") and not target.startswith("https://") and not target.startswith("localhost:") and not target.startswith("127.0.0.1:"):
target = f"localhost:{target}"
if not CADDYFILE_PATH.exists():
try:
CADDYFILE_PATH.parent.mkdir(parents=True, exist_ok=True)
CADDYFILE_PATH.write_text("# Caddyfile\n:80 {\n\troot * /usr/share/caddy\n\tfile_server\n}\n", encoding="utf-8")
except Exception as e:
return False, f"Failed to initialize Caddyfile: {e}", ""
try:
content = CADDYFILE_PATH.read_text(encoding="utf-8")
backup_content = content
block_pattern = re.compile(
rf"(?:^|\n)\s*{re.escape(host)}\s*\{{[^}}]*\}}",
re.MULTILINE | re.DOTALL,
)
new_block = f"\n{host} {{\n\treverse_proxy {target}\n}}\n"
if block_pattern.search(content):
new_content = block_pattern.sub(new_block.strip("\n"), content)
else:
new_content = content.rstrip() + "\n" + new_block
# Save and validate
CADDYFILE_PATH.write_text(new_content, encoding="utf-8")
# Validate with caddy CLI
proc = os.system(f"caddy validate --config {CADDYFILE_PATH} > /dev/null 2>&1")
if proc != 0:
CADDYFILE_PATH.write_text(backup_content, encoding="utf-8")
return False, "Caddy configuration validation failed. Reverted to previous state.", ""
# Reload Caddy service
reload_res = os.system("systemctl reload caddy > /dev/null 2>&1 || caddy reload --config /etc/caddy/Caddyfile > /dev/null 2>&1")
if reload_res != 0:
logger.warning("Caddy reload returned non-zero, trying restart...")
os.system("systemctl restart caddy > /dev/null 2>&1")
full_url = f"https://{host}"
return True, f"Reverse proxy for {host} configured successfully.", full_url
except Exception as e:
logger.error(f"Error configuring Caddy reverse proxy: {e}", exc_info=True)
return False, f"Error configuring Caddy: {str(e)}", ""
def remove_caddy_reverse_proxy(subdomain: str, custom_domain: Optional[str] = None) -> Tuple[bool, str]:
"""Removes a reverse proxy block from Caddyfile and reloads Caddy."""
sub = subdomain.strip().lower()
sub = re.sub(r"[^\w\-.]", "", sub)
if custom_domain:
host = custom_domain.strip().lower()
elif "." in sub:
host = sub
else:
host = f"{sub}.{WILDCARD_DOMAIN}"
if not CADDYFILE_PATH.exists():
return False, "Caddyfile does not exist."
try:
content = CADDYFILE_PATH.read_text(encoding="utf-8")
backup_content = content
block_pattern = re.compile(
rf"(?:^|\n)\s*{re.escape(host)}\s*\{{[^}}]*\}}",
re.MULTILINE | re.DOTALL,
)
if not block_pattern.search(content):
return False, f"ساب‌دامین {host} در Caddyfile یافت نشد."
new_content = block_pattern.sub("", content).strip() + "\n"
CADDYFILE_PATH.write_text(new_content, encoding="utf-8")
proc = os.system(f"caddy validate --config {CADDYFILE_PATH} > /dev/null 2>&1")
if proc != 0:
CADDYFILE_PATH.write_text(backup_content, encoding="utf-8")
return False, "Caddy validation failed after removal. Reverted."
os.system("systemctl reload caddy > /dev/null 2>&1 || caddy reload --config /etc/caddy/Caddyfile > /dev/null 2>&1")
return True, f"ساب‌دامین {host} با موفقیت از Caddy حذف گردید."
except Exception as e:
logger.error(f"Error removing Caddy subdomain: {e}", exc_info=True)
return False, f"Error removing Caddy subdomain: {str(e)}"
# =====================================================================
# Attribute & Tag Parser
# =====================================================================
def parse_tag_attributes(attrs_str: str) -> Dict[str, str]:
"""
Parses key="value", key='value', or key=value pairs from a tag attribute string.
If no key=value syntax is used (e.g. single positional argument), maps to '_default'.
"""
attrs: Dict[str, str] = {}
if not attrs_str or not attrs_str.strip():
return attrs
clean_str = attrs_str.strip()
# Check if this is a single quoted string e.g. "claude-sonnet-4-6" or 'my-project'
if (clean_str.startswith('"') and clean_str.endswith('"')) or (clean_str.startswith("'") and clean_str.endswith("'")):
attrs["_default"] = clean_str[1:-1].strip()
return attrs
# Check if single unquoted word
if "=" not in clean_str and " " not in clean_str:
attrs["_default"] = clean_str
return attrs
kv_pairs = re.findall(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s,\]]+))', clean_str)
for k, v1, v2, v3 in kv_pairs:
key = k.lower().strip()
val = v1 if v1 != "" else (v2 if v2 != "" else v3)
attrs[key] = val.strip()
return attrs
# =====================================================================
# Core Action Processor
# =====================================================================
ACTION_REGEX = re.compile(
r"\[\[([A-Z0-9_]+)(?::\s*([\s\S]*?))?\]\]",
re.MULTILINE,
)
async def execute_action(
action_name: str,
attrs: Dict[str, str],
chat_id: int,
current_project_name: str,
is_fa: bool,
app: Any = None,
) -> Tuple[str, List[Callable], Optional[ScheduledTask]]:
"""
Executes a single bot action based on action_name and attributes.
Returns (rendered_html_badge, list_of_async_side_effects, created_task_or_none).
"""
session = session_manager.get_or_create(chat_id)
is_admin = settings.is_admin(chat_id)
side_effects: List[Callable] = []
created_task: Optional[ScheduledTask] = None
act = action_name.upper().strip()
# -------------------------------------------------------------
# 1. SET_MODEL / CHANGE_MODEL / MODEL
# -------------------------------------------------------------
if act in ("SET_MODEL", "CHANGE_MODEL", "MODEL", "SWITCH_MODEL"):
raw_m = attrs.get("model") or attrs.get("name") or attrs.get("target") or attrs.get("_default", "")
req_p = attrs.get("project") or attrs.get("proj")
resolved_m = resolve_model_name(raw_m)
if not resolved_m:
err = (
f"\n⚠️ <b>خطا در تغییر مدل:</b> مدل «<code>{escape_html(raw_m)}</code>» معتبر نیست.\n"
f"مدل‌های معتبر: <code>{', '.join(AVAILABLE_MODELS.keys())}</code>"
if is_fa else
f"\n⚠️ <b>Model Change Failed:</b> Model '<code>{escape_html(raw_m)}</code>' is invalid.\n"
f"Valid models: <code>{', '.join(AVAILABLE_MODELS.keys())}</code>"
)
return err, side_effects, created_task
# Find target project dynamically
target_proj = None
if req_p:
accessible = session_manager.get_all_accessible_projects(chat_id)
for k, p in accessible.items():
if k.lower() == req_p.lower() or p.name.lower() == req_p.lower():
target_proj = p
break
if not target_proj:
target_proj = session_manager.get_current_project(chat_id)
if not target_proj:
return "\n⚠️ <i>پروژه‌ای برای تنظیم مدل یافت نشد.</i>" if is_fa else "\n⚠️ <i>No active project found to set model.</i>", side_effects, created_task
target_proj.model = resolved_m
# Check effort compatibility
valid_effort = get_valid_effort_for_model(resolved_m, target_proj.effort)
if valid_effort:
target_proj.effort = valid_effort
session_manager.save()
model_display = AVAILABLE_MODELS.get(resolved_m, resolved_m)
badge = (
f"\n\n🧠 <b>مدل هوش مصنوعی تغییر یافت:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(target_proj.name)}</code>\n"
f"• ⚡ <b>مدل جدید:</b> <code>{escape_html(model_display)}</code> (<code>{resolved_m}</code>)"
if is_fa else
f"\n\n🧠 <b>AI Model Switched:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(target_proj.name)}</code>\n"
f"• ⚡ <b>New Model:</b> <code>{escape_html(model_display)}</code> (<code>{resolved_m}</code>)"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 2. SET_EFFORT / EFFORT
# -------------------------------------------------------------
elif act in ("SET_EFFORT", "EFFORT", "REASONING_EFFORT"):
raw_eff = attrs.get("effort") or attrs.get("level") or attrs.get("_default", "")
req_p = attrs.get("project") or attrs.get("proj")
resolved_eff = resolve_effort_level(raw_eff)
if not resolved_eff:
return f"\n⚠️ سطح فکر «{raw_eff}» نامعتبر است (گزینه‌ها: low, medium, high)." if is_fa else f"\n⚠️ Invalid effort '{raw_eff}' (options: low, medium, high).", side_effects, created_task
target_proj = None
if req_p:
accessible = session_manager.get_all_accessible_projects(chat_id)
for k, p in accessible.items():
if k.lower() == req_p.lower() or p.name.lower() == req_p.lower():
target_proj = p
break
if not target_proj:
target_proj = session_manager.get_current_project(chat_id)
if not target_proj:
return "\n⚠️ پروژه‌ای یافت نشد." if is_fa else "\n⚠️ No project found.", side_effects, created_task
target_proj.effort = resolved_eff
session_manager.save()
badge = (
f"\n\n⚡ <b>سطح تفکر مدل (Effort) تنظیم شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(target_proj.name)}</code>\n"
f"• 🎯 <b>میزان تفکر:</b> <code>{resolved_eff.upper()}</code>"
if is_fa else
f"\n\n⚡ <b>Reasoning Effort Updated:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(target_proj.name)}</code>\n"
f"• 🎯 <b>Effort Level:</b> <code>{resolved_eff.upper()}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 3. SET_LANG / SET_LANGUAGE / LANGUAGE
# -------------------------------------------------------------
elif act in ("SET_LANG", "SET_LANGUAGE", "LANGUAGE", "LANG"):
raw_l = attrs.get("lang") or attrs.get("language") or attrs.get("code") or attrs.get("_default", "")
resolved_l = resolve_language_code(raw_l)
if not resolved_l:
return f"\n⚠️ کد زبان «{raw_l}» نامعتبر است." if is_fa else f"\n⚠️ Invalid language code '{raw_l}'.", side_effects, created_task
await session_manager.set_language(chat_id, resolved_l)
lang_display = AVAILABLE_LANGUAGES.get(resolved_l, resolved_l)
badge = (
f"\n\n🌐 <b>زبان پاسخ‌دهی تغییر یافت:</b>\n"
f"• 🗣️ <b>زبان جدید:</b> <code>{escape_html(lang_display)}</code>"
if is_fa else
f"\n\n🌐 <b>Response Language Changed:</b>\n"
f"• 🗣️ <b>New Language:</b> <code>{escape_html(lang_display)}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 4. NEW_PROJECT / CREATE_PROJECT
# -------------------------------------------------------------
elif act in ("NEW_PROJECT", "CREATE_PROJECT", "ADD_PROJECT"):
p_name = attrs.get("name") or attrs.get("project") or attrs.get("_default", "")
if not p_name:
return "\n⚠️ نام پروژه مشخص نشده است." if is_fa else "\n⚠️ Project name not specified.", side_effects, created_task
ws = attrs.get("workspace") or attrs.get("path")
req_model = resolve_model_name(attrs.get("model", ""))
req_effort = resolve_effort_level(attrs.get("effort", ""))
desc = attrs.get("description", "")
proj = await session_manager.create_project(
chat_id=chat_id,
name=p_name,
workspace=ws,
model=req_model,
effort=req_effort,
description=desc,
)
badge = (
f"\n\n📁 <b>پروژه جدید با موفقیت ایجاد و فعال شد:</b>\n"
f"• 🏷️ <b>نام پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 📂 <b>مسیر دایرکتوری:</b> <code>{escape_html(proj.workspace)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{proj.model}</code> | ⚡ <b>تفکر:</b> <code>{proj.effort}</code>"
if is_fa else
f"\n\n📁 <b>New Project Created & Activated:</b>\n"
f"• 🏷️ <b>Project Name:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(proj.workspace)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{proj.model}</code> | ⚡ <b>Effort:</b> <code>{proj.effort}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 5. SWITCH_PROJECT / SELECT_PROJECT / USE_PROJECT
# -------------------------------------------------------------
elif act in ("SWITCH_PROJECT", "SELECT_PROJECT", "USE_PROJECT", "CHANGE_PROJECT"):
target_name = attrs.get("name") or attrs.get("project") or attrs.get("_default", "")
if not target_name:
return "\n⚠️ نام پروژه برای سوییچ مشخص نشده است." if is_fa else "\n⚠️ Project name for switch not specified.", side_effects, created_task
proj = await session_manager.switch_project(chat_id, target_name)
if not proj:
return f"\n⚠️ پروژه با نام «{escape_html(target_name)}» یافت نشد." if is_fa else f"\n⚠️ Project '{escape_html(target_name)}' was not found.", side_effects, created_task
badge = (
f"\n\n🔄 <b>پروژه فعال با موفقیت تغییر کرد:</b>\n"
f"• 📁 <b>پروژه جدید:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 📂 <b>مسیر دایرکتوری:</b> <code>{escape_html(proj.workspace)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{proj.model}</code>"
if is_fa else
f"\n\n🔄 <b>Active Project Switched:</b>\n"
f"• 📁 <b>Current Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(proj.workspace)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{proj.model}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 6. DELETE_PROJECT / DEL_PROJECT
# -------------------------------------------------------------
elif act in ("DELETE_PROJECT", "DEL_PROJECT", "REMOVE_PROJECT"):
target_name = attrs.get("name") or attrs.get("project") or attrs.get("_default", "")
delete_files_str = attrs.get("delete_files", "false").lower()
del_files = delete_files_str in ("true", "1", "yes", "بله")
if not target_name:
return "\n⚠️ نام پروژه برای حذف مشخص نشده است." if is_fa else "\n⚠️ Project name not specified for deletion.", side_effects, created_task
# Get workspace before deleting if files should be deleted
proj_to_delete = session.projects.get(target_name)
ws_to_clean = proj_to_delete.workspace if (proj_to_delete and del_files) else None
ok, msg = await session_manager.delete_project(chat_id, target_name)
if not ok:
return f"\n⚠️ {msg}", side_effects, created_task
if ws_to_clean and str(ws_to_clean).startswith("/root/projects"):
try:
shutil.rmtree(ws_to_clean, ignore_errors=True)
msg += f"\n🗑️ <i>پوشه فایل‌ها ({escape_html(ws_to_clean)}) نیز پاکسازی شد.</i>" if is_fa else f"\n🗑️ <i>Files directory ({escape_html(ws_to_clean)}) deleted.</i>"
except Exception as e:
logger.warning(f"Error removing project files: {e}")
badge = (
f"\n\n🗑️ <b>عملیات حذف پروژه:</b>\n{msg}"
if is_fa else
f"\n\n🗑️ <b>Project Deletion:</b>\n{msg}"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 7. RENAME_PROJECT
# -------------------------------------------------------------
elif act in ("RENAME_PROJECT", "RENAME"):
old_n = attrs.get("old_name") or attrs.get("old")
new_n = attrs.get("new_name") or attrs.get("new") or attrs.get("name")
if not old_n or not new_n:
return "\n⚠️ لطفاً نام قدیم و جدید پروژه را مشخص کنید." if is_fa else "\n⚠️ Please specify both old and new project names.", side_effects, created_task
proj, msg = await session_manager.rename_project(chat_id, old_n, new_n)
return f"\n\n✏️ {msg}", side_effects, created_task
# -------------------------------------------------------------
# 8. SET_WORKSPACE / WORKSPACE
# -------------------------------------------------------------
elif act in ("SET_WORKSPACE", "WORKSPACE", "CHANGE_WORKSPACE"):
ws_path = attrs.get("path") or attrs.get("workspace") or attrs.get("dir") or attrs.get("_default", "")
req_p = attrs.get("project") or attrs.get("proj")
if not ws_path:
return "\n⚠️ مسیر دایرکتوری مشخص نشده است." if is_fa else "\n⚠️ Workspace path not specified.", side_effects, created_task
resolved_ws = str(Path(ws_path).expanduser().resolve())
os.makedirs(resolved_ws, exist_ok=True)
target_proj = None
if req_p:
accessible = session_manager.get_all_accessible_projects(chat_id)
for k, p in accessible.items():
if k.lower() == req_p.lower() or p.name.lower() == req_p.lower():
target_proj = p
break
if not target_proj:
target_proj = session_manager.get_current_project(chat_id)
if target_proj:
target_proj.workspace = resolved_ws
session_manager.save()
badge = (
f"\n\n📂 <b>مسیر کاری (Workspace) بروزرسانی شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(target_proj.name if target_proj else current_project_name)}</code>\n"
f"• 📍 <b>مسیر جدید:</b> <code>{escape_html(resolved_ws)}</code>"
if is_fa else
f"\n\n📂 <b>Workspace Updated:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(target_proj.name if target_proj else current_project_name)}</code>\n"
f"• 📍 <b>New Path:</b> <code>{escape_html(resolved_ws)}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 9. SHARE_PROJECT / UNSHARE_PROJECT
# -------------------------------------------------------------
elif act in ("SHARE_PROJECT", "SHARE"):
uid_str = attrs.get("user_id") or attrs.get("user") or attrs.get("target_id") or attrs.get("_default", "")
target_p = attrs.get("project")
try:
target_uid = int(uid_str)
ok, msg, _ = await session_manager.share_project(chat_id, target_uid, target_p)
return f"\n\n👥 {msg}", side_effects, created_task
except ValueError:
return f"\n⚠️ شناسه کاربری «{uid_str}» نامعتبر است." if is_fa else f"\n⚠️ Invalid user ID '{uid_str}'.", side_effects, created_task
elif act in ("UNSHARE_PROJECT", "UNSHARE", "REVOKE_SHARE"):
uid_str = attrs.get("user_id") or attrs.get("user") or attrs.get("target_id") or attrs.get("_default", "")
target_p = attrs.get("project")
try:
target_uid = int(uid_str)
ok, msg, _ = await session_manager.unshare_project(chat_id, target_uid, target_p)
return f"\n\n👥 {msg}", side_effects, created_task
except ValueError:
return f"\n⚠️ شناسه کاربری «{uid_str}» نامعتبر است." if is_fa else f"\n⚠️ Invalid user ID '{uid_str}'.", side_effects, created_task
# -------------------------------------------------------------
# 10. RESET_CONVERSATION / NEW_CONV / NEW_CHAT / CLEAR_CONTEXT
# -------------------------------------------------------------
elif act in ("RESET_CONVERSATION", "NEW_CONV", "NEW_CHAT", "CLEAR_CONTEXT", "RESET_CHAT"):
p_name = attrs.get("project")
await session_manager.reset_session(chat_id, p_name)
badge = (
"\n\n🧹 <b>نشست گفتگو با موفقیت پاکسازی شد و گفتگوی جدیدی آغاز گردید.</b>"
if is_fa else
"\n\n🧹 <b>Conversation session reset. Starting a fresh new chat context.</b>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 11. SWITCH_CONVERSATION / SWITCH_CHAT
# -------------------------------------------------------------
elif act in ("SWITCH_CONVERSATION", "SWITCH_CHAT", "OPEN_CONV"):
conv_id = attrs.get("id") or attrs.get("conv_id") or attrs.get("_default", "")
if not conv_id:
ok, msg, cid, _ = session_manager.reopen_last_conversation(chat_id)
return f"\n\n💬 {msg}", side_effects, created_task
else:
ok, msg, _ = session_manager.switch_conversation(chat_id, conv_id)
return f"\n\n💬 {msg}", side_effects, created_task
# -------------------------------------------------------------
# 12. DELETE_CONVERSATION / DEL_CONV
# -------------------------------------------------------------
elif act in ("DELETE_CONVERSATION", "DEL_CONV", "DELETE_CHAT"):
conv_id = attrs.get("id") or attrs.get("conv_id") or attrs.get("_default", "")
if not conv_id:
return "\n⚠️ شناسه گفتگو مشخص نشده است." if is_fa else "\n⚠️ Conversation ID not specified.", side_effects, created_task
ok, msg = session_manager.delete_conversation(chat_id, conv_id)
return f"\n\n🗑️ {msg}", side_effects, created_task
# -------------------------------------------------------------
# 13. CLEAR_CONVERSATIONS / CLEAR_CHATS
# -------------------------------------------------------------
elif act in ("CLEAR_CONVERSATIONS", "CLEAR_CHATS", "DELETE_ALL_CONVS"):
ok, msg, count = session_manager.clear_project_conversations(chat_id)
return f"\n\n🗑️ {msg}", side_effects, created_task
# -------------------------------------------------------------
# 14. SCHEDULE_TASK / SCHEDULE / CREATE_TASK
# -------------------------------------------------------------
elif act in ("SCHEDULE_TASK", "SCHEDULE", "CREATE_TASK", "ADD_TASK"):
timing = attrs.get("timing") or attrs.get("time") or attrs.get("cron") or ""
content = attrs.get("content") or attrs.get("prompt") or attrs.get("cmd") or attrs.get("command") or ""
title = attrs.get("title") or ""
task_type = (attrs.get("type") or "prompt").lower()
req_p = attrs.get("project") or attrs.get("proj")
if req_p:
target_p = req_p
else:
curr = session_manager.get_current_project(chat_id)
target_p = curr.name if curr else current_project_name
max_runs = None
runs_val = attrs.get("max_runs") or attrs.get("repeat") or attrs.get("count") or attrs.get("times")
if runs_val and runs_val.lower() not in ("none", "null", "unlimited", "0"):
try:
max_runs = int(runs_val)
except ValueError:
pass
if not timing or not content:
return "\n⚠️ برای زمان‌بندی تسک، مشخص کردن timing و content الزامی است." if is_fa else "\n⚠️ Timing and content are required for task scheduling.", side_effects, created_task
try:
task = task_scheduler.add_task(
chat_id=chat_id,
creator_id=chat_id,
project_name=target_p,
task_type=task_type,
content=content,
timing_spec=timing,
title=title,
max_runs=max_runs,
)
created_task = task
type_name = "پرامپت هوش مصنوعی" if task_type == "prompt" else "دستور شل" if task_type == "command" else "یادآور"
next_rel = format_relative_time(task.next_run_timestamp - time.time(), is_fa=is_fa)
repeat_str = f"{task.max_runs} بار" if task.max_runs else "نامحدود (دائمی)"
badge = (
f"\n\n⏰ <b>تسک زمان‌بندی شده توسط هوش مصنوعی ثبت شد:</b>\n"
f"• 🏷️ <b>شناسه تسک:</b> <code>{task.id}</code>\n"
f"• ⏱️ <b>زمان‌بندی:</b> <code>{escape_html(task.timing_spec)}</code>\n"
f"• 🔄 <b>حداکثر تکرار:</b> <code>{repeat_str}</code>\n"
f"• 🛠 <b>نوع:</b> {type_name}\n"
f"• ⏳ <b>اولین اجرا:</b> {next_rel} (<code>{format_timestamp(task.next_run_timestamp, is_fa=True)}</code>)\n"
f"• 📝 <b>محتوا:</b> <code>{escape_html(task.content[:120])}</code>"
if is_fa else
f"\n\n⏰ <b>Scheduled Task Registered:</b>\n"
f"• 🏷️ <b>Task ID:</b> <code>{task.id}</code>\n"
f"• ⏱️ <b>Timing:</b> <code>{escape_html(task.timing_spec)}</code>\n"
f"• 🔄 <b>Max Runs:</b> <code>{task.max_runs if task.max_runs else 'Unlimited'}</code>\n"
f"• 🛠 <b>Type:</b> {type_name}\n"
f"• ⏳ <b>First Run:</b> {next_rel} (<code>{format_timestamp(task.next_run_timestamp, is_fa=False)}</code>)\n"
f"• 📝 <b>Content:</b> <code>{escape_html(task.content[:120])}</code>"
)
return badge, side_effects, created_task
except Exception as e:
return f"\n⚠️ <i>خطا در ثبت تسک زمان‌بندی: {escape_html(str(e))}</i>", side_effects, created_task
# -------------------------------------------------------------
# 15. CANCEL_TASK / DELETE_TASK / PAUSE_TASK / RESUME_TASK
# -------------------------------------------------------------
elif act in ("CANCEL_TASK", "DELETE_TASK", "REMOVE_TASK"):
t_id = attrs.get("id") or attrs.get("task_id") or attrs.get("_default", "")
if not t_id:
return "\n⚠️ شناسه تسک مشخص نشده است." if is_fa else "\n⚠️ Task ID not specified.", side_effects, created_task
deleted = False
user_tasks = task_scheduler.get_user_tasks(chat_id)
for t in user_tasks:
if t.id == t_id or t.id.startswith(t_id) or (t.title and t.title.lower() == t_id.lower()):
task_scheduler.delete_task(t.id)
deleted = True
break
if deleted:
return f"\n\n🗑️ تسک زمان‌بندی <code>{escape_html(t_id)}</code> با موفقیت لغو و حذف شد." if is_fa else f"\n\n🗑️ Scheduled task <code>{escape_html(t_id)}</code> was deleted.", side_effects, created_task
else:
return f"\n⚠️ تسک <code>{escape_html(t_id)}</code> یافت نشد." if is_fa else f"\n⚠️ Task <code>{escape_html(t_id)}</code> not found.", side_effects, created_task
elif act in ("PAUSE_TASK", "STOP_TASK"):
t_id = attrs.get("id") or attrs.get("task_id") or attrs.get("_default", "")
ok = task_scheduler.pause_task(t_id)
if ok:
return f"\n\n⏸️ تسک زمان‌بندی <code>{escape_html(t_id)}</code> موقتاً متوقف (Pause) شد." if is_fa else f"\n\n⏸️ Task <code>{escape_html(t_id)}</code> paused.", side_effects, created_task
return f"\n⚠️ امکان توقف تسک <code>{escape_html(t_id)}</code> وجود ندارد." if is_fa else f"\n⚠️ Could not pause task <code>{escape_html(t_id)}</code>.", side_effects, created_task
elif act in ("RESUME_TASK", "START_TASK"):
t_id = attrs.get("id") or attrs.get("task_id") or attrs.get("_default", "")
ok = task_scheduler.resume_task(t_id)
if ok:
return f"\n\n▶️ تسک زمان‌بندی <code>{escape_html(t_id)}</code> مجدداً فعال شد." if is_fa else f"\n\n▶️ Task <code>{escape_html(t_id)}</code> resumed.", side_effects, created_task
return f"\n⚠️ امکان فعال‌سازی تسک <code>{escape_html(t_id)}</code> وجود ندارد." if is_fa else f"\n⚠️ Could not resume task <code>{escape_html(t_id)}</code>.", side_effects, created_task
# -------------------------------------------------------------
# 16. BACKUP_PROJECT / EXPORT_PROJECT
# -------------------------------------------------------------
elif act in ("BACKUP_PROJECT", "EXPORT_PROJECT", "ZIP_PROJECT"):
req_p = attrs.get("project") or attrs.get("proj")
proj = None
if req_p:
accessible = session_manager.get_all_accessible_projects(chat_id)
for k, p in accessible.items():
if k.lower() == req_p.lower() or p.name.lower() == req_p.lower():
proj = p
break
if not proj:
proj = session_manager.get_current_project(chat_id)
if not proj:
return "\n⚠️ پروژه‌ای برای پشتیبان‌گیری یافت نشد." if is_fa else "\n⚠️ No project found for backup.", side_effects, created_task
async def do_backup():
if app:
from bot import handle_project_backup_flow
await handle_project_backup_flow(
application=app,
chat_id=chat_id,
proj_name=proj.name,
workspace=proj.workspace,
is_fa=is_fa,
status_msg=None,
)
side_effects.append(do_backup)
badge = (
f"\n\n📦 <b>درخواست پشتیبان‌گیری از پروژه «{escape_html(proj.name)}» ثبت شد و فایل بکاپ برای شما ارسال می‌گردد.</b>"
if is_fa else
f"\n\n📦 <b>Backup requested for project '{escape_html(proj.name)}'. Zip file is being generated and sent.</b>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 17. SEND_FILE / SEND_DOCUMENT
# -------------------------------------------------------------
elif act in ("SEND_FILE", "SEND_DOCUMENT", "ATTACH_FILE"):
f_path = attrs.get("path") or attrs.get("file") or attrs.get("file_path") or attrs.get("_default", "")
caption = attrs.get("caption") or ""
resolved_p = Path(f_path).expanduser().resolve()
if not resolved_p.exists() or not resolved_p.is_file():
return f"\n⚠️ فایل <code>{escape_html(f_path)}</code> در سرور یافت نشد." if is_fa else f"\n⚠️ File <code>{escape_html(f_path)}</code> not found.", side_effects, created_task
async def send_doc():
if app and app.bot:
from telegram import constants
try:
with open(resolved_p, "rb") as df:
await app.bot.send_document(
chat_id=chat_id,
document=df,
filename=resolved_p.name,
caption=caption or f"📄 {resolved_p.name}",
parse_mode=constants.ParseMode.HTML if caption else None,
read_timeout=60,
write_timeout=60,
)
except Exception as ex:
logger.error(f"Failed to send file {resolved_p}: {ex}")
side_effects.append(send_doc)
badge = (
f"\n\n📄 <b>فایل <code>{escape_html(resolved_p.name)}</code> پیوست و ارسال گردید.</b>"
if is_fa else
f"\n\n📄 <b>File <code>{escape_html(resolved_p.name)}</code> attached and sent.</b>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 18. SEND_PHOTO / SEND_IMAGE
# -------------------------------------------------------------
elif act in ("SEND_PHOTO", "SEND_IMAGE"):
img_path = attrs.get("path") or attrs.get("file") or attrs.get("_default", "")
caption = attrs.get("caption") or ""
resolved_p = Path(img_path).expanduser().resolve()
if not resolved_p.exists() or not resolved_p.is_file():
return f"\n⚠️ تصویر <code>{escape_html(img_path)}</code> یافت نشد." if is_fa else f"\n⚠️ Image <code>{escape_html(img_path)}</code> not found.", side_effects, created_task
async def send_img():
if app and app.bot:
from telegram import constants
try:
with open(resolved_p, "rb") as pf:
await app.bot.send_photo(
chat_id=chat_id,
photo=pf,
caption=caption or None,
parse_mode=constants.ParseMode.HTML if caption else None,
)
except Exception as ex:
logger.error(f"Failed to send photo {resolved_p}: {ex}")
side_effects.append(send_img)
badge = (
f"\n\n🖼️ <b>تصویر <code>{escape_html(resolved_p.name)}</code> ارسال گردید.</b>"
if is_fa else
f"\n\n🖼️ <b>Photo <code>{escape_html(resolved_p.name)}</code> sent.</b>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 19. SEND_AUDIO / SEND_VOICE
# -------------------------------------------------------------
elif act in ("SEND_AUDIO", "SEND_VOICE"):
a_path = attrs.get("path") or attrs.get("file") or attrs.get("_default", "")
caption = attrs.get("caption") or ""
resolved_p = Path(a_path).expanduser().resolve()
if not resolved_p.exists() or not resolved_p.is_file():
return f"\n⚠️ فایل صوتی <code>{escape_html(a_path)}</code> یافت نشد." if is_fa else f"\n⚠️ Audio file <code>{escape_html(a_path)}</code> not found.", side_effects, created_task
async def send_aud():
if app and app.bot:
try:
with open(resolved_p, "rb") as af:
if act == "SEND_VOICE":
await app.bot.send_voice(chat_id=chat_id, voice=af, caption=caption or None)
else:
await app.bot.send_audio(chat_id=chat_id, audio=af, caption=caption or None)
except Exception as ex:
logger.error(f"Failed to send audio {resolved_p}: {ex}")
side_effects.append(send_aud)
return f"\n\n🎵 <b>فایل صوتی <code>{escape_html(resolved_p.name)}</code> ارسال شد.</b>", side_effects, created_task
# -------------------------------------------------------------
# 20. SETUP_SUBDOMAIN / ADD_SUBDOMAIN / CADDY_PROXY
# -------------------------------------------------------------
elif act in ("SETUP_SUBDOMAIN", "ADD_SUBDOMAIN", "CADDY_PROXY", "EXPOSE_PORT"):
sub = attrs.get("subdomain") or attrs.get("name") or attrs.get("host") or ""
port = attrs.get("port") or attrs.get("target") or attrs.get("_default", "")
custom_domain = attrs.get("custom_domain")
if not sub or not port:
return "\n⚠️ برای تنظیم ساب‌دامین، مشخص کردن subdomain و port الزامی است." if is_fa else "\n⚠️ Subdomain and port are required for proxy setup.", side_effects, created_task
ok, msg, full_url = configure_caddy_reverse_proxy(
subdomain=sub,
port_or_target=port,
custom_domain=custom_domain,
)
if ok:
badge = (
f"\n\n🌐 <b>ساب‌دامین و Reverse Proxy با موفقیت راه‌اندازی شد:</b>\n"
f"• 🔗 <b>آدرس وب‌سایت:</b> <a href=\"{full_url}\">{full_url}</a>\n"
f"• 🔌 <b>متصل به پورت داخلی:</b> <code>{escape_html(str(port))}</code>\n"
f"• 🔒 <b>گواهی امنیتی SSL (HTTPS):</b> فعال و خودکار (Caddy)"
if is_fa else
f"\n\n🌐 <b>Subdomain & Reverse Proxy Configured:</b>\n"
f"• 🔗 <b>Website URL:</b> <a href=\"{full_url}\">{full_url}</a>\n"
f"• 🔌 <b>Proxied Port:</b> <code>{escape_html(str(port))}</code>\n"
f"• 🔒 <b>SSL Certificate (HTTPS):</b> Active & Automatic (Caddy)"
)
return badge, side_effects, created_task
else:
return f"\n⚠️ <i>خطا در پیکربندی Caddy: {escape_html(msg)}</i>", side_effects, created_task
# -------------------------------------------------------------
# 21. REMOVE_SUBDOMAIN / DEL_SUBDOMAIN
# -------------------------------------------------------------
elif act in ("REMOVE_SUBDOMAIN", "DEL_SUBDOMAIN", "DELETE_SUBDOMAIN"):
sub = attrs.get("subdomain") or attrs.get("name") or attrs.get("_default", "")
if not sub:
return "\n⚠️ ساب‌دامین برای حذف مشخص نشده است." if is_fa else "\n⚠️ Subdomain not specified.", side_effects, created_task
ok, msg = remove_caddy_reverse_proxy(sub)
return f"\n\n🌐 {msg}", side_effects, created_task
# -------------------------------------------------------------
# 22. INVITE_USER / ADD_USER (Admin Only)
# -------------------------------------------------------------
elif act in ("INVITE_USER", "ADD_USER", "WHITELIST_USER"):
if not is_admin:
return "\n⚠️ فقط مدیران دسترسی به افزودن کاربر دارند." if is_fa else "\n⚠️ Only admins can whitelist users.", side_effects, created_task
uid_str = attrs.get("user_id") or attrs.get("user") or attrs.get("_default", "")
note = attrs.get("note", "")
try:
target_uid = int(uid_str)
settings.add_authorized_user(target_uid)
invite_manager.approve_request(target_uid)
return f"\n\n✅ کاربر <code>{target_uid}</code> با موفقیت به لیست مجاز افزوده شد.", side_effects, created_task
except ValueError:
return f"\n⚠️ شناسه کاربری «{uid_str}» نامعتبر است.", side_effects, created_task
# -------------------------------------------------------------
# 23. REMOVE_USER / BAN_USER (Admin Only)
# -------------------------------------------------------------
elif act in ("REMOVE_USER", "BAN_USER", "REVOKE_USER"):
if not is_admin:
return "\n⚠️ فقط مدیران دسترسی به حذف کاربر دارند." if is_fa else "\n⚠️ Only admins can remove users.", side_effects, created_task
uid_str = attrs.get("user_id") or attrs.get("user") or attrs.get("_default", "")
try:
target_uid = int(uid_str)
settings.remove_authorized_user(target_uid)
return f"\n\n🚫 دسترسی کاربر <code>{target_uid}</code> با موفقیت لغو شد.", side_effects, created_task
except ValueError:
return f"\n⚠️ شناسه کاربری «{uid_str}» نامعتبر است.", side_effects, created_task
# -------------------------------------------------------------
# 24. RESTART_BOT / RELOAD_BOT
# -------------------------------------------------------------
elif act in ("RESTART_BOT", "RELOAD_BOT"):
async def do_restart():
start_w = time.time()
while time.time() - start_w < 180.0:
in_prog = any(s.turn_in_progress for s in session_manager.sessions.values())
if not in_prog and not session_manager.active_tasks and not session_manager.active_procs:
break
await asyncio.sleep(1.5)
await asyncio.sleep(2.0)
os.system("systemctl restart agy-telegram-bot.service")
side_effects.append(do_restart)
badge = (
"\n\n🔄 <b>دستور راه‌اندازی مجدد هوشمند ثبت شد (پس از اتمام کامل عملیات جاری، ربات ری‌استارت می‌گردد).</b>"
if is_fa else
"\n\n🔄 <b>Graceful restart scheduled (will execute automatically after all active tasks complete).</b>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 25. GIT_SYNC / GIT_PULL_AND_PUSH
# -------------------------------------------------------------
elif act in ("GIT_SYNC", "SYNC_GIT", "GITEA_SYNC"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else curr_proj
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
from git_manager import git_manager
msg_commit = attrs.get("message") or f"Sync from AI Bot for {proj.name}"
ok, res_msg = await git_manager.git_sync(proj.workspace, message=msg_commit, repo_name=proj.name)
urls = git_manager.get_repo_urls(proj.name)
badge = (
f"\n\n🔄 <b>عملیات همگام‌سازی مخزن گیت (Git Sync) با موفقیت انجام شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>مخزن تحت وب:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📋 <b>گزارش عملیات:</b>\n{res_msg}"
if is_fa else
f"\n\n🔄 <b>Git Repository Synced Successfully:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>Web Repo:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📋 <b>Details:</b>\n{res_msg}"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 26. GIT_COMMIT / GIT_PUSH
# -------------------------------------------------------------
elif act in ("GIT_COMMIT", "GIT_PUSH", "COMMIT_PROJECT"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else curr_proj
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
from git_manager import git_manager
commit_msg = attrs.get("message") or attrs.get("_default") or f"Updates for {proj.name}"
ok, res_msg = await git_manager.git_commit_and_push(proj.workspace, message=commit_msg, repo_name=proj.name)
urls = git_manager.get_repo_urls(proj.name)
badge = (
f"\n\n💾 <b>عملیات کامیت و پوش گیت:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>مشاهده در گیت:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📝 {res_msg}"
if is_fa else
f"\n\n💾 <b>Git Commit & Push Result:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>View in Git:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📝 {res_msg}"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 27. GIT_INIT / CREATE_REPO
# -------------------------------------------------------------
elif act in ("GIT_INIT", "INIT_REPO", "CREATE_REPO"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else curr_proj
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
from git_manager import git_manager
res = await git_manager.init_project_repo(proj.workspace, proj.name)
urls = git_manager.get_repo_urls(proj.name)
if res.get("success"):
badge = (
f"\n\n🎉 <b>مخزن گیت پروژه با موفقیت در Gitea ایجاد و متصل شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>آدرس وب:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📥 <b>دستور کلون:</b> <code>git clone {urls['clone_url']}</code>"
if is_fa else
f"\n\n🎉 <b>Git Repository Created & Linked on Gitea:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>Web URL:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📥 <b>Clone:</b> <code>git clone {urls['clone_url']}</code>"
)
else:
badge = f"\n⚠️ خطا در راه‌اندازی مخزن گیت: {escape_html(str(res.get('error')))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 28. GIT_CHECKOUT
# -------------------------------------------------------------
elif act in ("GIT_CHECKOUT", "CHECKOUT_GIT", "GIT_SWITCH"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else curr_proj
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
target = attrs.get("commit") or attrs.get("target") or attrs.get("branch") or attrs.get("_default") or "main"
from git_manager import git_manager
ok, msg = await git_manager.git_checkout(proj.workspace, target)
if ok:
badge = (
f"\n\n🔀 <b>سوییچ شاخه/کامیت گیت انجام شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🎯 <b>مقصد:</b> <code>{escape_html(target)}</code>\n"
f"• 📋 {escape_html(msg)}"
if is_fa else
f"\n\n🔀 <b>Git Checkout Succeeded:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🎯 <b>Target:</b> <code>{escape_html(target)}</code>\n"
f"• 📋 {escape_html(msg)}"
)
else:
badge = f"\n⚠️ خطا در Checkout گیت: {escape_html(msg)}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 29. GIT_RESET
# -------------------------------------------------------------
elif act in ("GIT_RESET", "RESET_GIT", "GIT_ROLLBACK"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else curr_proj
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
commit_h = attrs.get("commit") or attrs.get("hash") or attrs.get("_default")
if not commit_h:
return "\n⚠️ لطفاً شناسه کامیت مقصد را مشخص نمایید." if is_fa else "\n⚠️ Please specify the target commit hash.", side_effects, created_task
from git_manager import git_manager
ok, msg = await git_manager.git_reset_hard(proj.workspace, commit_h)
if ok:
badge = (
f"\n\n⏪ <b>پروژه با موفقیت به کامیت مورد نظر بازگردانی شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🔑 <b>کامیت:</b> <code>{escape_html(commit_h)}</code>\n"
f"• 📝 {msg}"
if is_fa else
f"\n\n⏪ <b>Git Reset Completed:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🔑 <b>Commit:</b> <code>{escape_html(commit_h)}</code>\n"
f"• 📝 {msg}"
)
else:
badge = f"\n⚠️ خطا در بازگردانی گیت: {escape_html(msg)}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 30. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
# -------------------------------------------------------------
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
from memory_manager import memory_manager
mem_type = (attrs.get("type") or "private").lower().strip()
key = attrs.get("key") or attrs.get("slug") or attrs.get("name")
content = attrs.get("content") or attrs.get("text") or attrs.get("value") or attrs.get("_default")
category = (attrs.get("category") or "general").lower().strip()
importance_raw = attrs.get("importance") or "1"
try:
importance = int(importance_raw)
except Exception:
importance = 1
if not key or not content:
return (
"\n⚠️ خطا در ذخیره خاطره: کلید (key) و محتوا (content) الزامی هستند."
if is_fa
else "\n⚠️ Memory save error: 'key' and 'content' are required.",
side_effects,
created_task,
)
# Security check: Non-admins cannot create global memories
if mem_type == "global" and not is_admin:
logger.warning("Non-admin user %s attempted to create global memory %s; downgraded to private.", chat_id, key)
mem_type = "private"
try:
item, is_created = memory_manager.save_or_update(
type_=mem_type,
key=key,
content=content,
user_id=chat_id,
category=category,
importance=importance,
created_by=chat_id,
)
type_label = "🌐 عمومی (سیستم)" if item.is_global else "👤 خصوصی (شما)"
badge = (
f"\n\n🧠 <b>خاطره هوش مصنوعی ثبت شد:</b>\n"
f"• 🏷️ <b>نوع:</b> {type_label}\n"
f"• 🔑 <b>کلید:</b> <code>{escape_html(item.key)}</code>\n"
f"• 📂 <b>دسته:</b> <code>{escape_html(item.category)}</code>\n"
f"• 📝 <b>محتوا:</b> {escape_html(item.content)}"
if is_fa
else
f"\n\n🧠 <b>AI Memory Saved:</b>\n"
f"• 🏷️ <b>Type:</b> {'Global' if item.is_global else 'Private'}\n"
f"• 🔑 <b>Key:</b> <code>{escape_html(item.key)}</code>\n"
f"• 📂 <b>Category:</b> <code>{escape_html(item.category)}</code>\n"
f"• 📝 <b>Content:</b> {escape_html(item.content)}"
)
except Exception as e:
badge = f"\n⚠️ خطا در ذخیره خاطره: {escape_html(str(e))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 31. UPDATE_MEMORY
# -------------------------------------------------------------
elif act in ("UPDATE_MEMORY", "EDIT_MEMORY"):
from memory_manager import memory_manager
mem_type = (attrs.get("type") or "private").lower().strip()
key = attrs.get("key") or attrs.get("slug")
content = attrs.get("content") or attrs.get("text") or attrs.get("_default")
category = attrs.get("category")
importance_raw = attrs.get("importance")
if not key or not content:
return (
"\n⚠️ خطا در به‌روزرسانی خاطره: مشخص کردن key و content الزامی است."
if is_fa
else "\n⚠️ Memory update error: 'key' and 'content' required.",
side_effects,
created_task,
)
if mem_type == "global" and not is_admin:
mem_type = "private"
try:
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id)
cat = category or (existing.category if existing else "general")
imp = int(importance_raw) if importance_raw and importance_raw.isdigit() else (existing.importance if existing else 1)
item, is_created = memory_manager.save_or_update(
type_=mem_type,
key=key,
content=content,
user_id=chat_id,
category=cat,
importance=imp,
created_by=chat_id,
)
type_label = "🌐 عمومی (سیستم)" if item.is_global else "👤 خصوصی (شما)"
badge = (
f"\n\n🧠 <b>خاطره هوش مصنوعی به‌روزرسانی شد:</b>\n"
f"• 🏷️ <b>نوع:</b> {type_label}\n"
f"• 🔑 <b>کلید:</b> <code>{escape_html(item.key)}</code>\n"
f"• 📝 <b>محتوای جدید:</b> {escape_html(item.content)}"
if is_fa
else
f"\n\n🧠 <b>AI Memory Updated:</b>\n"
f"• 🏷️ <b>Type:</b> {'Global' if item.is_global else 'Private'}\n"
f"• 🔑 <b>Key:</b> <code>{escape_html(item.key)}</code>\n"
f"• 📝 <b>New Content:</b> {escape_html(item.content)}"
)
except Exception as e:
badge = f"\n⚠️ خطا در ویرایش خاطره: {escape_html(str(e))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 32. DELETE_MEMORY / FORGET_MEMORY / REMOVE_MEMORY
# -------------------------------------------------------------
elif act in ("DELETE_MEMORY", "FORGET_MEMORY", "REMOVE_MEMORY"):
from memory_manager import memory_manager
mem_id_raw = attrs.get("id")
key = attrs.get("key") or attrs.get("slug") or attrs.get("_default")
mem_type = (attrs.get("type") or "private").lower().strip()
try:
if mem_id_raw and mem_id_raw.isdigit():
ok = memory_manager.delete_by_id(int(mem_id_raw), user_id=chat_id, is_admin=is_admin)
elif key:
ok = memory_manager.delete_by_key(type_=mem_type, key=key, user_id=chat_id, is_admin=is_admin)
else:
return "\n⚠️ لطفاً شناسه یا کلید خاطره جهت حذف را مشخص کنید." if is_fa else "\n⚠️ Please specify memory id or key to delete.", side_effects, created_task
if ok:
badge = f"\n\n🗑️ <b>خاطره مورد نظر از حافظه هوش مصنوعی حذف گردید.</b>" if is_fa else f"\n\n🗑️ <b>Memory forgotten successfully.</b>"
else:
badge = f"\n⚠️ خاطره‌ای با این مشخصات یافت نشد." if is_fa else f"\n⚠️ Memory not found."
except PermissionError:
badge = "\n🚫 <i>خطای دسترسی: ویرایش یا حذف خاطرات عمومی تنها توسط مدیر سیستم امکان‌پذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only administrators can modify global memories.</i>"
except Exception as e:
badge = f"\n⚠️ خطا در حذف خاطره: {escape_html(str(e))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 33. CLEAR_MEMORY / PURGE_MEMORY
# -------------------------------------------------------------
elif act in ("CLEAR_MEMORY", "PURGE_MEMORY", "RESET_MEMORY"):
from memory_manager import memory_manager
mem_type = (attrs.get("type") or "private").lower().strip()
try:
count = memory_manager.clear_memories(type_=mem_type, user_id=chat_id, is_admin=is_admin)
type_label = "عمومی" if mem_type == "global" else "خصوصی"
badge = (
f"\n\n🧹 <b>حافظه {type_label} پاک‌سازی شد ({count} مورد حذف گردید).</b>"
if is_fa
else
f"\n\n🧹 <b>{mem_type.capitalize()} memory cleared ({count} items purged).</b>"
)
except PermissionError:
badge = "\n🚫 <i>خطای دسترسی: پاک‌سازی خاطرات عمومی تنها توسط مدیر امکان‌پذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only admins can clear global memories.</i>"
except Exception as e:
badge = f"\n⚠️ خطا در پاک‌سازی حافظه: {escape_html(str(e))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# Auto-effort internal tag
# -------------------------------------------------------------
elif act == "ESCALATE_EFFORT":
return "", side_effects, created_task
# Unknown tag
return f"\n<i>[اکشن ناشناخته: {action_name}]</i>" if is_fa else f"\n<i>[Unknown action: {action_name}]</i>", side_effects, created_task
# =====================================================================
# Main AI Response Processor
# =====================================================================
async def process_all_ai_actions(
raw_text: str,
chat_id: int,
project_name: str,
is_fa: bool,
app: Any = None,
) -> Tuple[str, List[ScheduledTask], List[str]]:
"""
Scans AI response text for ALL action tags: [[ACTION_NAME: key="value", ...]]
Executes each action, gathers side-effects, replaces tags with formatted Telegram cards,
and executes background side-effects.
"""
if not raw_text or "[[" not in raw_text:
return raw_text, [], []
created_tasks: List[ScheduledTask] = []
executed_actions: List[str] = []
all_side_effects: List[Callable] = []
matches = list(ACTION_REGEX.finditer(raw_text))
if not matches:
return raw_text, [], []
processed_text = raw_text
for m in matches:
full_tag = m.group(0)
action_name = m.group(1).upper().strip()
attrs_raw = m.group(2) or ""
attrs = parse_tag_attributes(attrs_raw)
try:
rendered_badge, side_effects, task = await execute_action(
action_name=action_name,
attrs=attrs,
chat_id=chat_id,
current_project_name=project_name,
is_fa=is_fa,
app=app,
)
if task:
created_tasks.append(task)
all_side_effects.extend(side_effects)
executed_actions.append(action_name)
# Replace tag with badge
processed_text = processed_text.replace(full_tag, rendered_badge)
except Exception as ex:
logger.error(f"Error executing AI action tag {full_tag}: {ex}", exc_info=True)
err_msg = f"\n⚠️ <i>(خطا در اجرای عملکرد ربات: {escape_html(str(ex))})</i>" if is_fa else f"\n⚠️ <i>(Bot action error: {escape_html(str(ex))})</i>"
processed_text = processed_text.replace(full_tag, err_msg)
# Execute all side-effects in background
for side_effect in all_side_effects:
asyncio.create_task(side_effect())
return processed_text, created_tasks, executed_actions
# =====================================================================
# System Instructions for AGY Agent
# =====================================================================
def get_ai_bot_actions_instruction(lang: str = "fa") -> str:
"""
Constructs the comprehensive system instruction empowering the AI to invoke ALL bot functions.
"""
return (
"[SYSTEM INSTRUCTION: TELEGRAM BOT ACTIONS & FUNCTION CALLING]\n"
"You have complete access and authority to control and invoke ALL Telegram Bot features, project settings, model parameters, scheduled tasks, reverse proxies, and server operations directly from your conversation.\n"
"Whenever the user asks (in Persian, English, or any language) to change settings, switch models, create or manage projects, schedule periodic tasks, configure subdomains on Caddy, export backups, restart the bot, or send files:\n"
"You MUST output the corresponding action tag(s) anywhere in your reply. You can combine multiple action tags in a single reply if necessary.\n\n"
"AVAILABLE ACTION TAGS & SYNTAX:\n"
"1. Change AI Model for Project:\n"
" [[SET_MODEL: model=\"<model_id>\", project=\"<project_name_optional>\"]]\n"
" Available models: 'gemini-3.7-flash-auto' (Gemini 3.7 Flash Auto Reasoning ⚡/🧠 - ارزیابی هوشمند خودکار), 'gemini-3.7-flash' (Gemini 3.7 Flash), 'gemini-3.7-flash-high' (Gemini 3.7 Flash High), 'gemini-3.1-pro' (Gemini 3.1 Pro), 'gemini-3.6-flash', 'claude-sonnet-4-6' (Claude Sonnet 4.6 Thinking), 'claude-opus-4-6-thinking' (Claude Opus 4.6 Thinking), 'gpt-oss-120b'.\n"
" Example: [[SET_MODEL: model=\"gemini-3.7-flash-auto\"]]\n\n"
"2. Change Reasoning Effort Level:\n"
" [[SET_EFFORT: effort=\"low|medium|high\", project=\"<project_name_optional>\"]]\n"
" Example: [[SET_EFFORT: effort=\"high\"]]\n\n"
"3. Change Bot Language:\n"
" [[SET_LANG: lang=\"fa|en|auto|...\"]]\n"
" Example: [[SET_LANG: lang=\"fa\"]]\n\n"
"4. Create New Project:\n"
" [[NEW_PROJECT: name=\"<project_name>\", workspace=\"<optional_path>\", model=\"<optional_model>\", effort=\"<optional_effort>\"]]\n"
" (Directory `/root/projects/{user_id}/{name}` is created automatically).\n"
" Example: [[NEW_PROJECT: name=\"blog\"]]\n\n"
"5. Switch Active Project:\n"
" [[SWITCH_PROJECT: name=\"<project_name>\"]]\n"
" Example: [[SWITCH_PROJECT: name=\"blog\"]]\n\n"
"6. Delete Project (Comprehensive Teardown & Resource Release):\n"
" [[DELETE_PROJECT: name=\"<project_name>\", delete_files=true|false]]\n"
" (Automatically performs comprehensive teardown: releases Caddy subdomains/proxies, stops background processes/ports, removes systemd services, cancels scheduled cron tasks, deletes Gitea git repository, and purges physical files).\n"
" Example: [[DELETE_PROJECT: name=\"old-project\", delete_files=true]]\n\n"
"7. Rename Project:\n"
" [[RENAME_PROJECT: old_name=\"<old>\", new_name=\"<new>\"]]\n\n"
"8. Set Custom Workspace Directory:\n"
" [[SET_WORKSPACE: path=\"/path/to/folder\", project=\"<project_optional>\"]]\n\n"
"9. Share / Unshare Project:\n"
" [[SHARE_PROJECT: user_id=<telegram_id>, project=\"<project_optional>\"]]\n"
" [[UNSHARE_PROJECT: user_id=<telegram_id>, project=\"<project_optional>\"]]\n\n"
"10. Reset / Clear Conversation Context:\n"
" [[RESET_CONVERSATION: project=\"<project_optional>\"]]\n\n"
"11. Switch / Delete / Clear Conversations:\n"
" [[SWITCH_CONVERSATION: id=\"<conv_id_or_empty_for_last>\"]]\n"
" [[DELETE_CONVERSATION: id=\"<conv_id>\"]]\n"
" [[CLEAR_CONVERSATIONS: project=\"<project_optional>\"]]\n\n"
"12. Schedule Task / Timers / Cron:\n"
" [[SCHEDULE_TASK: timing=\"<timing_spec>\", max_runs=<count_or_null>, type=\"prompt|command|reminder\", title=\"<short_title>\", content=\"<exact_prompt_or_command>\"]]\n"
" Timing examples: 'every 10m', 'in 15m', '14:30', 'cron: 0 23 * * *', 'every 1h (3 times)'.\n"
" Types: 'prompt' (AI run), 'command' (bash script), 'reminder' (plain notification).\n"
" [[CANCEL_TASK: id=\"<task_id>\"]]\n"
" [[PAUSE_TASK: id=\"<task_id>\"]]\n"
" [[RESUME_TASK: id=\"<task_id>\"]]\n\n"
"13. Project Backup & Export (Zip):\n"
" [[BACKUP_PROJECT: project=\"<project_optional>\"]]\n\n"
"14. Send File / Photo / Audio to Telegram:\n"
" [[SEND_FILE: path=\"/path/to/file.ext\", caption=\"<caption_text>\"]]\n"
" [[SEND_PHOTO: path=\"/path/to/image.png\", caption=\"<caption_text>\"]]\n"
" [[SEND_AUDIO: path=\"/path/to/audio.mp3\", caption=\"<caption_text>\"]]\n\n"
"15. Configure Subdomain & Caddy HTTPS Reverse Proxy (*.msa.artacloud.ir):\n"
" [[SETUP_SUBDOMAIN: subdomain=\"<subdomain_name>\", port=<port_number>]]\n"
" Automatically links `{subdomain}.msa.artacloud.ir` to `localhost:{port}` with free SSL.\n"
" [[REMOVE_SUBDOMAIN: subdomain=\"<subdomain_name>\"]]\n\n"
"16. Git Version Control & Gitea Integration:\n"
" [[GIT_SYNC: project=\"<project_optional>\", message=\"<optional_commit_message>\"]]\n"
" (Pulls remote changes from Gitea, commits any local modifications, and pushes to main branch).\n"
" [[GIT_COMMIT: message=\"<commit_message>\", project=\"<project_optional>\"]]\n"
" [[GIT_INIT: project=\"<project_optional>\"]]\n\n"
"17. Restart Telegram Bot Service:\n"
" [[RESTART_BOT]]\n\n"
"18. Long-Term AI Memory Management:\n"
" [[SAVE_MEMORY: type=\"private|global\", key=\"<unique_slug>\", category=\"<category>\", content=\"<concise_summary>\"]]\n"
" (Saves durable facts/preferences. type defaults to 'private'. Only admin can create 'global').\n"
" [[UPDATE_MEMORY: key=\"<slug>\", content=\"<new_content>\"]]\n"
" [[DELETE_MEMORY: key=\"<slug>\"]]\n"
" [[CLEAR_MEMORY: type=\"private|global\"]]\n\n"
"Always explain to the user in a friendly and professional tone what actions have been performed."
)