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
import uuid
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, WebAppInfo
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
# =====================================================================
# AI Dynamic Button Storage & Helpers
# =====================================================================
AI_BUTTON_PAYLOADS: Dict[str, Dict[str, Any]] = {}
def register_ai_button_payload(btn_type: str, value: str, chat_id: int, project: str) -> str:
"""Registers an AI action payload and returns a compact 8-char ID for Telegram callback_data."""
now = time.time()
# Clean up old payloads (older than 24h)
if len(AI_BUTTON_PAYLOADS) > 2000:
expired = [k for k, v in AI_BUTTON_PAYLOADS.items() if now - v.get("time", 0) > 86400]
for k in expired:
AI_BUTTON_PAYLOADS.pop(k, None)
btn_id = uuid.uuid4().hex[:8]
AI_BUTTON_PAYLOADS[btn_id] = {
"type": btn_type,
"value": value,
"chat_id": chat_id,
"project": project,
"time": now,
}
return btn_id
def get_ai_button_payload(btn_id: str) -> Optional[Dict[str, Any]]:
"""Retrieves registered button payload data by its ID."""
return AI_BUTTON_PAYLOADS.get(btn_id)
def create_inline_button_from_spec(
spec: Dict[str, Any],
chat_id: int,
project_name: str,
) -> Optional[InlineKeyboardButton]:
"""Creates a Telegram InlineKeyboardButton from a button specification dictionary."""
text = spec.get("text") or spec.get("title") or spec.get("label") or spec.get("name") or spec.get("_default", "")
text = str(text).strip()
if not text:
return None
# 1. URL button
url = spec.get("url") or spec.get("link") or spec.get("href")
if url:
url_str = str(url).strip()
if not (url_str.startswith("http://") or url_str.startswith("https://") or url_str.startswith("tg://")):
url_str = f"https://{url_str}"
return InlineKeyboardButton(text=text, url=url_str)
# 2. Web App button
web_app = spec.get("web_app") or spec.get("webapp")
if web_app:
return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=str(web_app).strip()))
# 3. Prompt button (Send prompt back to AI agent)
prompt = spec.get("prompt") or spec.get("ask") or spec.get("query")
if prompt:
btn_id = register_ai_button_payload("prompt", str(prompt).strip(), chat_id, project_name)
return InlineKeyboardButton(text=text, callback_data=f"aibtn:{btn_id}")
# 4. Command button (Run slash command or bash)
cmd = spec.get("cmd") or spec.get("command") or spec.get("run")
if cmd:
btn_id = register_ai_button_payload("cmd", str(cmd).strip(), chat_id, project_name)
return InlineKeyboardButton(text=text, callback_data=f"aibtn:{btn_id}")
# 5. Callback / action data
cb_data = spec.get("data") or spec.get("callback") or spec.get("callback_data") or spec.get("action")
if cb_data:
cb_str = str(cb_data).strip()
if len(cb_str.encode("utf-8")) <= 60 and not cb_str.startswith("aibtn:"):
return InlineKeyboardButton(text=text, callback_data=cb_str)
else:
btn_id = register_ai_button_payload("action", cb_str, chat_id, project_name)
return InlineKeyboardButton(text=text, callback_data=f"aibtn:{btn_id}")
# Default fallback: Treat text as prompt
btn_id = register_ai_button_payload("prompt", text, chat_id, project_name)
return InlineKeyboardButton(text=text, callback_data=f"aibtn:{btn_id}")
def _extract_json_from_text(raw_text: str) -> Optional[Any]:
"""Attempts to parse JSON list/dict from a raw string with high fault tolerance."""
raw = raw_text.strip()
# Check if starts with layout= or buttons=
m_kv = re.search(r'(?:layout|buttons|keyboard)\s*=\s*([\[\{][\s\S]*[\]\}])', raw, re.IGNORECASE)
if m_kv:
raw = m_kv.group(1).strip()
elif not (raw.startswith("[") or raw.startswith("{")):
# Try finding [ ... ] block
m_arr = re.search(r'(\[[\s\S]*\])', raw)
if m_arr:
raw = m_arr.group(1).strip()
# Attempt standard JSON parse
try:
return json.loads(raw)
except Exception:
pass
# Attempt cleanup for Python style dicts / single quotes
try:
cleaned = raw.replace("'", '"')
cleaned = re.sub(r',\s*([\]\}])', r'\1', cleaned) # Remove trailing commas
return json.loads(cleaned)
except Exception:
pass
return None
def extract_action_tags(text: str) -> List[Tuple[str, str, str]]:
"""
Extracts all [[ACTION_NAME: ...]] or [[ACTION_NAME]] tags with bracket-depth balancing.
Returns list of (full_tag_string, action_name, raw_attributes_string).
"""
tags = []
i = 0
n = len(text)
while i < n - 1:
if text[i:i+2] == "[[" and (i == 0 or text[i-1] != "\\"):
start = i
# Find action name (starts right after [[)
j = i + 2
while j < n and text[j] not in (":", "]", " ", "\n", "\t"):
j += 1
action_name = text[i+2:j].strip()
# Start tracking inside tag
k = j
if k < n and text[k] == ":":
k += 1
attrs_start = k
depth = 1 # 1 for outer [[ ... ]]
while k < n:
if k < n - 1 and text[k:k+2] == "]]" and depth == 1:
full_tag = text[start:k+2]
attrs_raw = text[attrs_start:k]
tags.append((full_tag, action_name, attrs_raw))
i = k + 2
break
elif text[k] == "[":
depth += 1
k += 1
elif text[k] == "]":
depth -= 1
k += 1
else:
k += 1
else:
i += 1
else:
i += 1
return tags
def parse_buttons_from_text(
raw_text: str,
chat_id: int,
project_name: str,
) -> Tuple[str, Optional[InlineKeyboardMarkup]]:
"""
Finds and extracts button tags ([[BUTTON:...]], [[BUTTONS:...]], [[INLINE_BUTTONS:...]], [[KEYBOARD:...]])
from the raw AI response text, builds the InlineKeyboardMarkup, and cleans the text.
"""
if not raw_text or "[[" not in raw_text:
return raw_text, None
cleaned_text = raw_text
row_map: Dict[int, List[InlineKeyboardButton]] = {}
unnumbered_buttons: List[InlineKeyboardButton] = []
matrix_rows: List[List[InlineKeyboardButton]] = []
tags = extract_action_tags(raw_text)
if not tags:
return raw_text, None
for full_tag, tag_name, attrs_raw in tags:
tag_type = tag_name.upper().strip()
if tag_type not in ("BUTTON", "BUTTONS", "INLINE_BUTTONS", "KEYBOARD", "INLINE_KEYBOARD"):
continue
# Remove the tag from output text
cleaned_text = cleaned_text.replace(full_tag, "")
attrs_raw = attrs_raw.strip()
if tag_type in ("BUTTONS", "INLINE_BUTTONS", "KEYBOARD", "INLINE_KEYBOARD"):
parsed_json = _extract_json_from_text(attrs_raw)
if isinstance(parsed_json, list):
# Could be 2D array or 1D array
if parsed_json and isinstance(parsed_json[0], list):
# 2D layout: [[btn1, btn2], [btn3]]
for r in parsed_json:
row_items = []
if isinstance(r, list):
for b_spec in r:
if isinstance(b_spec, dict):
btn = create_inline_button_from_spec(b_spec, chat_id, project_name)
if btn:
row_items.append(btn)
if row_items:
matrix_rows.append(row_items)
else:
# 1D layout: [btn1, btn2, btn3, ...] -> chunk into rows of 2
cur_row = []
for b_spec in parsed_json:
if isinstance(b_spec, dict):
btn = create_inline_button_from_spec(b_spec, chat_id, project_name)
if btn:
cur_row.append(btn)
if len(cur_row) >= 2:
matrix_rows.append(cur_row)
cur_row = []
if cur_row:
matrix_rows.append(cur_row)
else:
# Try fallback key-value parsing
attrs = parse_tag_attributes(attrs_raw)
btn = create_inline_button_from_spec(attrs, chat_id, project_name)
if btn:
unnumbered_buttons.append(btn)
elif tag_type == "BUTTON":
attrs = parse_tag_attributes(attrs_raw)
row_idx = None
if "row" in attrs:
try:
row_idx = int(attrs["row"])
except Exception:
pass
elif "line" in attrs:
try:
row_idx = int(attrs["line"])
except Exception:
pass
btn = create_inline_button_from_spec(attrs, chat_id, project_name)
if btn:
if row_idx is not None and row_idx > 0:
if row_idx not in row_map:
row_map[row_idx] = []
row_map[row_idx].append(btn)
else:
unnumbered_buttons.append(btn)
# Assemble all buttons into final keyboard
final_rows: List[List[InlineKeyboardButton]] = []
# 1. Add matrix rows first
if matrix_rows:
final_rows.extend(matrix_rows)
# 2. Add numbered rows
if row_map:
for r_num in sorted(row_map.keys()):
final_rows.append(row_map[r_num])
# 3. Add unnumbered buttons (chunked into pairs)
if unnumbered_buttons:
cur_row = []
for btn in unnumbered_buttons:
cur_row.append(btn)
if len(cur_row) >= 2:
final_rows.append(cur_row)
cur_row = []
if cur_row:
final_rows.append(cur_row)
# Clean up empty lines from text
cleaned_text = re.sub(r'\n{3,}', '\n\n', cleaned_text).strip()
if not final_rows:
return cleaned_text, None
return cleaned_text, InlineKeyboardMarkup(final_rows)
# =====================================================================
# 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⚠️ خطا در تغییر مدل: مدل «{escape_html(raw_m)}» معتبر نیست.\n"
f"مدلهای معتبر: {', '.join(AVAILABLE_MODELS.keys())}"
if is_fa else
f"\n⚠️ Model Change Failed: Model '{escape_html(raw_m)}' is invalid.\n"
f"Valid models: {', '.join(AVAILABLE_MODELS.keys())}"
)
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⚠️ پروژهای برای تنظیم مدل یافت نشد." if is_fa else "\n⚠️ No active project found to set model.", 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🧠 مدل هوش مصنوعی تغییر یافت:\n"
f"• 📁 پروژه: {escape_html(target_proj.name)}\n"
f"• ⚡ مدل جدید: {escape_html(model_display)} ({resolved_m})"
if is_fa else
f"\n\n🧠 AI Model Switched:\n"
f"• 📁 Project: {escape_html(target_proj.name)}\n"
f"• ⚡ New Model: {escape_html(model_display)} ({resolved_m})"
)
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⚡ سطح تفکر مدل (Effort) تنظیم شد:\n"
f"• 📁 پروژه: {escape_html(target_proj.name)}\n"
f"• 🎯 میزان تفکر: {resolved_eff.upper()}"
if is_fa else
f"\n\n⚡ Reasoning Effort Updated:\n"
f"• 📁 Project: {escape_html(target_proj.name)}\n"
f"• 🎯 Effort Level: {resolved_eff.upper()}"
)
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🌐 زبان پاسخدهی تغییر یافت:\n"
f"• 🗣️ زبان جدید: {escape_html(lang_display)}"
if is_fa else
f"\n\n🌐 Response Language Changed:\n"
f"• 🗣️ New Language: {escape_html(lang_display)}"
)
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📁 پروژه جدید با موفقیت ایجاد و فعال شد:\n"
f"• 🏷️ نام پروژه: {escape_html(proj.name)}\n"
f"• 📂 مسیر دایرکتوری: {escape_html(proj.workspace)}\n"
f"• 🧠 مدل: {proj.model} | ⚡ تفکر: {proj.effort}"
if is_fa else
f"\n\n📁 New Project Created & Activated:\n"
f"• 🏷️ Project Name: {escape_html(proj.name)}\n"
f"• 📂 Workspace: {escape_html(proj.workspace)}\n"
f"• 🧠 Model: {proj.model} | ⚡ Effort: {proj.effort}"
)
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🔄 پروژه فعال با موفقیت تغییر کرد:\n"
f"• 📁 پروژه جدید: {escape_html(proj.name)}\n"
f"• 📂 مسیر دایرکتوری: {escape_html(proj.workspace)}\n"
f"• 🧠 مدل: {proj.model}"
if is_fa else
f"\n\n🔄 Active Project Switched:\n"
f"• 📁 Current Project: {escape_html(proj.name)}\n"
f"• 📂 Workspace: {escape_html(proj.workspace)}\n"
f"• 🧠 Model: {proj.model}"
)
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🗑️ پوشه فایلها ({escape_html(ws_to_clean)}) نیز پاکسازی شد." if is_fa else f"\n🗑️ Files directory ({escape_html(ws_to_clean)}) deleted."
except Exception as e:
logger.warning(f"Error removing project files: {e}")
badge = (
f"\n\n🗑️ عملیات حذف پروژه:\n{msg}"
if is_fa else
f"\n\n🗑️ Project Deletion:\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📂 مسیر کاری (Workspace) بروزرسانی شد:\n"
f"• 📁 پروژه: {escape_html(target_proj.name if target_proj else current_project_name)}\n"
f"• 📍 مسیر جدید: {escape_html(resolved_ws)}"
if is_fa else
f"\n\n📂 Workspace Updated:\n"
f"• 📁 Project: {escape_html(target_proj.name if target_proj else current_project_name)}\n"
f"• 📍 New Path: {escape_html(resolved_ws)}"
)
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🧹 نشست گفتگو با موفقیت پاکسازی شد و گفتگوی جدیدی آغاز گردید."
if is_fa else
"\n\n🧹 Conversation session reset. Starting a fresh new chat context."
)
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⏰ تسک زمانبندی شده توسط هوش مصنوعی ثبت شد:\n"
f"• 🏷️ شناسه تسک: {task.id}\n"
f"• ⏱️ زمانبندی: {escape_html(task.timing_spec)}\n"
f"• 🔄 حداکثر تکرار: {repeat_str}\n"
f"• 🛠 نوع: {type_name}\n"
f"• ⏳ اولین اجرا: {next_rel} ({format_timestamp(task.next_run_timestamp, is_fa=True)})\n"
f"• 📝 محتوا: {escape_html(task.content[:120])}"
if is_fa else
f"\n\n⏰ Scheduled Task Registered:\n"
f"• 🏷️ Task ID: {task.id}\n"
f"• ⏱️ Timing: {escape_html(task.timing_spec)}\n"
f"• 🔄 Max Runs: {task.max_runs if task.max_runs else 'Unlimited'}\n"
f"• 🛠 Type: {type_name}\n"
f"• ⏳ First Run: {next_rel} ({format_timestamp(task.next_run_timestamp, is_fa=False)})\n"
f"• 📝 Content: {escape_html(task.content[:120])}"
)
return badge, side_effects, created_task
except Exception as e:
return f"\n⚠️ خطا در ثبت تسک زمانبندی: {escape_html(str(e))}", 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🗑️ تسک زمانبندی {escape_html(t_id)} با موفقیت لغو و حذف شد." if is_fa else f"\n\n🗑️ Scheduled task {escape_html(t_id)} was deleted.", side_effects, created_task
else:
return f"\n⚠️ تسک {escape_html(t_id)} یافت نشد." if is_fa else f"\n⚠️ Task {escape_html(t_id)} 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⏸️ تسک زمانبندی {escape_html(t_id)} موقتاً متوقف (Pause) شد." if is_fa else f"\n\n⏸️ Task {escape_html(t_id)} paused.", side_effects, created_task
return f"\n⚠️ امکان توقف تسک {escape_html(t_id)} وجود ندارد." if is_fa else f"\n⚠️ Could not pause task {escape_html(t_id)}.", 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▶️ تسک زمانبندی {escape_html(t_id)} مجدداً فعال شد." if is_fa else f"\n\n▶️ Task {escape_html(t_id)} resumed.", side_effects, created_task
return f"\n⚠️ امکان فعالسازی تسک {escape_html(t_id)} وجود ندارد." if is_fa else f"\n⚠️ Could not resume task {escape_html(t_id)}.", 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📦 درخواست پشتیبانگیری از پروژه «{escape_html(proj.name)}» ثبت شد و فایل بکاپ برای شما ارسال میگردد."
if is_fa else
f"\n\n📦 Backup requested for project '{escape_html(proj.name)}'. Zip file is being generated and sent."
)
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⚠️ فایل {escape_html(f_path)} در سرور یافت نشد." if is_fa else f"\n⚠️ File {escape_html(f_path)} 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📄 فایل {escape_html(resolved_p.name)} پیوست و ارسال گردید."
if is_fa else
f"\n\n📄 File {escape_html(resolved_p.name)} attached and sent."
)
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⚠️ تصویر {escape_html(img_path)} یافت نشد." if is_fa else f"\n⚠️ Image {escape_html(img_path)} 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🖼️ تصویر {escape_html(resolved_p.name)} ارسال گردید."
if is_fa else
f"\n\n🖼️ Photo {escape_html(resolved_p.name)} sent."
)
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⚠️ فایل صوتی {escape_html(a_path)} یافت نشد." if is_fa else f"\n⚠️ Audio file {escape_html(a_path)} 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🎵 فایل صوتی {escape_html(resolved_p.name)} ارسال شد.", 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🌐 سابدامین و Reverse Proxy با موفقیت راهاندازی شد:\n"
f"• 🔗 آدرس وبسایت: {full_url}\n"
f"• 🔌 متصل به پورت داخلی: {escape_html(str(port))}\n"
f"• 🔒 گواهی امنیتی SSL (HTTPS): فعال و خودکار (Caddy)"
if is_fa else
f"\n\n🌐 Subdomain & Reverse Proxy Configured:\n"
f"• 🔗 Website URL: {full_url}\n"
f"• 🔌 Proxied Port: {escape_html(str(port))}\n"
f"• 🔒 SSL Certificate (HTTPS): Active & Automatic (Caddy)"
)
return badge, side_effects, created_task
else:
return f"\n⚠️ خطا در پیکربندی Caddy: {escape_html(msg)}", 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✅ کاربر {target_uid} با موفقیت به لیست مجاز افزوده شد.", 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🚫 دسترسی کاربر {target_uid} با موفقیت لغو شد.", 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🔄 دستور راهاندازی مجدد هوشمند ثبت شد (پس از اتمام کامل عملیات جاری، ربات ریاستارت میگردد)."
if is_fa else
"\n\n🔄 Graceful restart scheduled (will execute automatically after all active tasks complete)."
)
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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} 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🔄 عملیات همگامسازی مخزن گیت (Git Sync) با موفقیت انجام شد:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🌐 مخزن تحت وب: {urls['web_url']}\n"
f"• 📋 گزارش عملیات:\n{res_msg}"
if is_fa else
f"\n\n🔄 Git Repository Synced Successfully:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🌐 Web Repo: {urls['web_url']}\n"
f"• 📋 Details:\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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} 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💾 عملیات کامیت و پوش گیت:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🌐 مشاهده در گیت: {urls['web_url']}\n"
f"• 📝 {res_msg}"
if is_fa else
f"\n\n💾 Git Commit & Push Result:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🌐 View in Git: {urls['web_url']}\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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} 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🎉 مخزن گیت پروژه با موفقیت در Gitea ایجاد و متصل شد:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🌐 آدرس وب: {urls['web_url']}\n"
f"• 📥 دستور کلون: git clone {urls['clone_url']}"
if is_fa else
f"\n\n🎉 Git Repository Created & Linked on Gitea:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🌐 Web URL: {urls['web_url']}\n"
f"• 📥 Clone: git clone {urls['clone_url']}"
)
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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} 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🔀 سوییچ شاخه/کامیت گیت انجام شد:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🎯 مقصد: {escape_html(target)}\n"
f"• 📋 {escape_html(msg)}"
if is_fa else
f"\n\n🔀 Git Checkout Succeeded:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🎯 Target: {escape_html(target)}\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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} 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⏪ پروژه با موفقیت به کامیت مورد نظر بازگردانی شد:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🔑 کامیت: {escape_html(commit_h)}\n"
f"• 📝 {msg}"
if is_fa else
f"\n\n⏪ Git Reset Completed:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🔑 Commit: {escape_html(commit_h)}\n"
f"• 📝 {msg}"
)
else:
badge = f"\n⚠️ خطا در بازگردانی گیت: {escape_html(msg)}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 30. GIT_REVERT / GIT_UNDO
# -------------------------------------------------------------
elif act in ("GIT_REVERT", "REVERT_GIT", "GIT_UNDO", "UNDO_GIT", "UNDO"):
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⚠️ پروژه {escape_html(p_name or '')} یافت نشد." if is_fa else f"\n⚠️ Project {escape_html(p_name or '')} not found.", side_effects, created_task
target = attrs.get("commit") or attrs.get("target") or attrs.get("_default") or "HEAD"
from git_manager import git_manager
ok, msg, extra = await git_manager.git_revert(proj.workspace, commit_target=target)
if ok:
badge = (
f"\n\n↩️ عملیات لغو تغییرات (Git Revert) با موفقیت انجام شد:\n"
f"• 📁 پروژه: {escape_html(proj.name)}\n"
f"• 🎯 کامیت هدف: {escape_html(target)}\n"
f"• 📋 {msg}"
if is_fa else
f"\n\n↩️ Git Revert Succeeded:\n"
f"• 📁 Project: {escape_html(proj.name)}\n"
f"• 🎯 Target Commit: {escape_html(target)}\n"
f"• 📋 {msg}"
)
else:
badge = f"\n⚠️ خطا در لغو کامیت: {msg}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 31. 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"📁 پروژه اختصاصی ({escape_html(proj_name or '')})"
elif item.is_user:
type_label = "👤 کاربر (سراسری برای شما)"
else:
type_label = "🌐 عمومی (سیستم)"
badge = (
f"\n\n🧠 خاطره هوش مصنوعی ثبت شد:\n"
f"• 🏷️ بخش: {type_label}\n"
f"• 🔑 کلید: {escape_html(item.key)}\n"
f"• 📂 دسته: {escape_html(item.category)}\n"
f"• 📝 محتوا: {escape_html(item.content)}"
if is_fa
else
f"\n\n🧠 AI Memory Saved:\n"
f"• 🏷️ Scope: {'Project (' + str(proj_name) + ')' if item.is_project else 'User'} \n"
f"• 🔑 Key: {escape_html(item.key)}\n"
f"• 📂 Category: {escape_html(item.category)}\n"
f"• 📝 Content: {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"📁 پروژه ({escape_html(proj_name or '')})" if item.is_project else "👤 کاربر (سراسری)"
badge = (
f"\n\n🧠 خاطره هوش مصنوعی بهروزرسانی شد:\n"
f"• 🏷️ بخش: {type_label}\n"
f"• 🔑 کلید: {escape_html(item.key)}\n"
f"• 📝 محتوای جدید: {escape_html(item.content)}"
if is_fa
else
f"\n\n🧠 AI Memory Updated:\n"
f"• 🏷️ Scope: {type_label}\n"
f"• 🔑 Key: {escape_html(item.key)}\n"
f"• 📝 New Content: {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🗑️ خاطره مورد نظر از حافظه هوش مصنوعی حذف گردید." if is_fa else f"\n\n🗑️ Memory forgotten successfully."
else:
badge = f"\n⚠️ خاطرهای با این مشخصات یافت نشد." if is_fa else f"\n⚠️ Memory not found."
except PermissionError:
badge = "\n🚫 خطای دسترسی: فقط مالک یا مدیر میتواند این خاطره را حذف کند." if is_fa else "\n🚫 Access Denied."
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🚫 خطای دسترسی: پاکسازی خاطرات عمومی تنها توسط مدیر امکانپذیر است." if is_fa else "\n🚫 Access Denied: Only admins can clear global memories.", 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🧹 حافظه {type_label} پاکسازی شد ({count} مورد حذف گردید)."
if is_fa
else
f"\n\n🧹 {type_label} memory cleared ({count} items purged)."
)
except PermissionError:
badge = "\n🚫 خطای دسترسی: عملیات مجاز نیست." if is_fa else "\n🚫 Access Denied."
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
# -------------------------------------------------------------
# Dynamic Buttons internal tags (extracted during pre-pass)
# -------------------------------------------------------------
elif act in ("BUTTON", "BUTTONS", "INLINE_BUTTONS", "KEYBOARD", "INLINE_KEYBOARD"):
return "", side_effects, created_task
# Unknown tag
return f"\n[اکشن ناشناخته: {action_name}]" if is_fa else f"\n[Unknown action: {action_name}]", 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], Optional[InlineKeyboardMarkup]]:
"""
Scans AI response text for ALL action tags: [[ACTION_NAME: key="value", ...]]
Executes each action, gathers side-effects, replaces tags with formatted Telegram cards,
extracts interactive inline buttons, and executes background side-effects.
Returns (processed_text, created_tasks, executed_actions, inline_markup).
"""
if not raw_text or "[[" not in raw_text:
return raw_text, [], [], None
# 1. Parse and extract all interactive button tags first
processed_text, inline_markup = parse_buttons_from_text(
raw_text=raw_text,
chat_id=chat_id,
project_name=project_name,
)
created_tasks: List[ScheduledTask] = []
executed_actions: List[str] = []
all_side_effects: List[Callable] = []
matches = list(ACTION_REGEX.finditer(processed_text))
if not matches:
return processed_text, [], [], inline_markup
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⚠️ (خطا در اجرای عملکرد ربات: {escape_html(str(ex))})" if is_fa else f"\n⚠️ (Bot action error: {escape_html(str(ex))})"
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, inline_markup
# =====================================================================
# 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=\"\", project=\"\"]]\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=\"\"]]\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=\"\", workspace=\"\", model=\"\", 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=\"\"]]\n"
" Example: [[SWITCH_PROJECT: name=\"blog\"]]\n\n"
"6. Delete Project (Comprehensive Teardown & Resource Release):\n"
" [[DELETE_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=\"\", new_name=\"\"]]\n\n"
"8. Set Custom Workspace Directory:\n"
" [[SET_WORKSPACE: path=\"/path/to/folder\", project=\"\"]]\n\n"
"9. Share / Unshare Project:\n"
" [[SHARE_PROJECT: user_id=, project=\"\"]]\n"
" [[UNSHARE_PROJECT: user_id=, project=\"\"]]\n\n"
"10. Reset / Clear Conversation Context:\n"
" [[RESET_CONVERSATION: project=\"\"]]\n\n"
"11. Switch / Delete / Clear Conversations:\n"
" [[SWITCH_CONVERSATION: id=\"\"]]\n"
" [[DELETE_CONVERSATION: id=\"\"]]\n"
" [[CLEAR_CONVERSATIONS: project=\"\"]]\n\n"
"12. Schedule Task / Timers / Cron:\n"
" [[SCHEDULE_TASK: timing=\"\", max_runs=, type=\"prompt|command|reminder\", title=\"\", content=\"\"]]\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=\"\"]]\n"
" [[PAUSE_TASK: id=\"\"]]\n"
" [[RESUME_TASK: id=\"\"]]\n\n"
"13. Project Backup & Export (Zip):\n"
" [[BACKUP_PROJECT: project=\"\"]]\n\n"
"14. Send File / Photo / Audio to Telegram:\n"
" [[SEND_FILE: path=\"/path/to/file.ext\", caption=\"\"]]\n"
" [[SEND_PHOTO: path=\"/path/to/image.png\", caption=\"\"]]\n"
" [[SEND_AUDIO: path=\"/path/to/audio.mp3\", caption=\"\"]]\n\n"
"15. Configure Subdomain & Caddy HTTPS Reverse Proxy (*.msa.artacloud.ir):\n"
" [[SETUP_SUBDOMAIN: subdomain=\"\", port=]]\n"
" Automatically links `{subdomain}.msa.artacloud.ir` to `localhost:{port}` with free SSL.\n"
" [[REMOVE_SUBDOMAIN: subdomain=\"\"]]\n\n"
"16. Git Version Control & Gitea Integration:\n"
" [[GIT_SYNC: project=\"\", message=\"\"]]\n"
" (Pulls remote changes from Gitea, commits any local modifications, and pushes to main branch).\n"
" [[GIT_COMMIT: message=\"\", project=\"\"]]\n"
" [[GIT_INIT: project=\"\"]]\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=\"\", category=\"\", content=\"\"]]\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=\"\", content=\"\"]]\n"
" [[DELETE_MEMORY: type=\"user|project\", key=\"\"]]\n"
" [[CLEAR_MEMORY: type=\"user|project\"]]\n\n"
"19. Interactive Inline Buttons (دکمههای شیشهای تعاملی زیر پیام نهایی):\n"
" You can attach interactive buttons with URLs, follow-up prompts, slash commands, or custom actions underneath your final response to give the user quick one-tap actions:\n"
" • Single button tag syntax:\n"
" [[BUTTON: text=\"\", url=\"\", prompt=\"\", cmd=\"\", row=]]\n"
" Examples:\n"
" [[BUTTON: text=\"🌐 مشاهده وبسایت\", url=\"https://app.msa.artacloud.ir\", row=1]]\n"
" [[BUTTON: text=\"🐙 مخزن گیت\", url=\"https://git.msa.artacloud.ir/root/my-app\", row=1]]\n"
" [[BUTTON: text=\"🔄 اجرای مجدد تستها\", prompt=\"تستهای برنامه را دوباره اجرا کن\", row=2]]\n"
" [[BUTTON: text=\"📊 وضعیت مصرف\", cmd=\"/usage\", row=2]]\n\n"
" • Grid / Matrix layout tag syntax (JSON layout):\n"
" [[BUTTONS: layout=[\n"
" [{\"text\": \"🚀 مشاهده سایت\", \"url\": \"https://app.msa.artacloud.ir\"}, {\"text\": \"📂 مخزن گیت\", \"url\": \"https://git.msa.artacloud.ir/root/my-app\"}],\n"
" [{\"text\": \"🔄 تست مجدد\", \"prompt\": \"تستها را دوباره اجرا کن\"}, {\"text\": \"📊 سهمیه مصرف\", \"cmd\": \"/usage\"}]\n"
" ]]]\n\n"
"Always explain to the user in a friendly and professional tone what actions have been performed."
)