1450 lines
73 KiB
Python
1450 lines
73 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 --no-block 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
|
|
|
|
raw_type = (attrs.get("type") or "user").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"
|
|
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
|
|
|
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,
|
|
)
|
|
|
|
# Map types: 'project' -> project tier, 'user'/'private' -> user tier, 'global' -> rejected/downgraded for AI
|
|
if raw_type in ("project", "proj"):
|
|
mem_type = "project"
|
|
proj_name = req_project
|
|
elif raw_type == "global":
|
|
# Global memories are strictly bot-managed by Admin. AI cannot auto-save global memories.
|
|
logger.info("AI attempted global memory save for user %s; safely converting to user memory.", chat_id)
|
|
mem_type = "user"
|
|
proj_name = None
|
|
else:
|
|
mem_type = "user"
|
|
proj_name = None
|
|
|
|
try:
|
|
item, is_created = memory_manager.save_or_update(
|
|
type_=mem_type,
|
|
key=key,
|
|
content=content,
|
|
user_id=chat_id,
|
|
project_name=proj_name,
|
|
category=category,
|
|
importance=importance,
|
|
created_by=chat_id,
|
|
)
|
|
if item.is_project:
|
|
type_label = f"📁 پروژه اختصاصی (<code>{escape_html(proj_name or '')}</code>)"
|
|
elif item.is_user:
|
|
type_label = "👤 کاربر (سراسری برای شما)"
|
|
else:
|
|
type_label = "🌐 عمومی (سیستم)"
|
|
|
|
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>Scope:</b> {'Project (' + str(proj_name) + ')' if item.is_project else 'User'} \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
|
|
|
|
raw_type = (attrs.get("type") or "user").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")
|
|
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
|
|
|
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 raw_type in ("project", "proj"):
|
|
mem_type = "project"
|
|
proj_name = req_project
|
|
else:
|
|
mem_type = "user"
|
|
proj_name = None
|
|
|
|
try:
|
|
existing = memory_manager.get_by_key(type_=mem_type, key=key, user_id=chat_id, project_name=proj_name)
|
|
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,
|
|
project_name=proj_name,
|
|
category=cat,
|
|
importance=imp,
|
|
created_by=chat_id,
|
|
)
|
|
type_label = f"📁 پروژه (<code>{escape_html(proj_name or '')}</code>)" if item.is_project 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>Scope:</b> {type_label}\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")
|
|
raw_type = (attrs.get("type") or "user").lower().strip()
|
|
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
|
|
|
mem_type = "project" if raw_type in ("project", "proj") else "user"
|
|
proj_name = req_project if mem_type == "project" else None
|
|
|
|
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, project_name=proj_name, 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.</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
|
|
|
|
raw_type = (attrs.get("type") or "user").lower().strip()
|
|
req_project = attrs.get("project") or attrs.get("proj") or current_project_name
|
|
|
|
if raw_type in ("project", "proj"):
|
|
mem_type = "project"
|
|
proj_name = req_project
|
|
type_label = f"پروژه {proj_name}"
|
|
elif raw_type == "global":
|
|
if not is_admin:
|
|
return "\n🚫 <i>خطای دسترسی: پاکسازی خاطرات عمومی تنها توسط مدیر امکانپذیر است.</i>" if is_fa else "\n🚫 <i>Access Denied: Only admins can clear global memories.</i>", side_effects, created_task
|
|
mem_type = "global"
|
|
proj_name = None
|
|
type_label = "عمومی سیستم"
|
|
else:
|
|
mem_type = "user"
|
|
proj_name = None
|
|
type_label = "شخصی کاربر"
|
|
|
|
try:
|
|
count = memory_manager.clear_memories(
|
|
type_=mem_type, user_id=chat_id, project_name=proj_name, is_admin=is_admin
|
|
)
|
|
badge = (
|
|
f"\n\n🧹 <b>حافظه {type_label} پاکسازی شد ({count} مورد حذف گردید).</b>"
|
|
if is_fa
|
|
else
|
|
f"\n\n🧹 <b>{type_label} memory cleared ({count} items purged).</b>"
|
|
)
|
|
except PermissionError:
|
|
badge = "\n🚫 <i>خطای دسترسی: عملیات مجاز نیست.</i>" if is_fa else "\n🚫 <i>Access Denied.</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 (3-Tier: User & Project):\n"
|
|
" [[SAVE_MEMORY: type=\"user|project\", key=\"<unique_slug>\", category=\"<category>\", content=\"<concise_summary>\"]]\n"
|
|
" (Use type='user' for personal user profile/habits, or type='project' for current project architecture/decisions. Global memory is read-only for AI).\n"
|
|
" [[UPDATE_MEMORY: type=\"user|project\", key=\"<slug>\", content=\"<new_content>\"]]\n"
|
|
" [[DELETE_MEMORY: type=\"user|project\", key=\"<slug>\"]]\n"
|
|
" [[CLEAR_MEMORY: type=\"user|project\"]]\n\n"
|
|
"Always explain to the user in a friendly and professional tone what actions have been performed."
|
|
)
|