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

8403 lines
442 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import re
import json
import time
import shutil
import asyncio
import logging
from pathlib import Path
from typing import Optional
from datetime import datetime
from telegram import (
Update,
InlineKeyboardButton,
InlineKeyboardMarkup,
constants,
BotCommand,
)
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
CallbackQueryHandler,
ContextTypes,
filters,
)
from telegram.error import TelegramError, BadRequest, RetryAfter
from config import settings, ENV_FILE
from agy_engine import (
AGYEngine,
AgentResult,
session_manager,
AVAILABLE_MODELS,
AVAILABLE_EFFORTS,
AVAILABLE_LANGUAGES,
strip_ansi,
get_conversation_last_output,
get_conversation_metadata,
clean_user_prompt,
)
from formatters import (
markdown_to_telegram_html,
format_thought,
format_tool_call,
format_context_stats,
get_model_display_name,
format_git_info,
format_git_commits_page,
format_commit_detail_view,
format_ftp_info,
format_ssh_info,
split_message,
escape_html,
)
from scheduler import (
task_scheduler,
ScheduledTask,
parse_timing_string,
format_relative_time,
format_timestamp,
normalize_digits,
)
from invite_manager import (
invite_manager,
InviteToken,
PendingRequest,
)
from backup_manager import backup_manager
from git_manager import git_manager
from ftp_manager import ftp_manager
from ssh_manager import ssh_manager
from remote_audit import remote_audit
from sys_monitor import render_server_hardware_report
from usage_monitor import fetch_and_render_usage_report
from bot_actions import process_all_ai_actions
from auth_manager import auth_manager
from web_uploader import create_upload_token, get_upload_url, start_web_uploader_server, set_telegram_app
LANG_ALIASES = {
"farsi": "fa",
"persian": "fa",
"فارسی": "fa",
"parsi": "fa",
"english": "en",
"indonesian": "id",
"indonesia": "id",
"bahasa": "id",
"spanish": "es",
"espanol": "es",
"japanese": "ja",
"chinese": "zh",
"german": "de",
"deutsch": "de",
"french": "fr",
"francais": "fr",
"russian": "ru",
"arabic": "ar",
"عربي": "ar",
"portuguese": "pt",
"korean": "ko",
"italian": "it",
"turkish": "tr",
"vietnamese": "vi",
"dutch": "nl",
}
def get_lang_display(lang_code_or_name: str) -> str:
if not lang_code_or_name:
return AVAILABLE_LANGUAGES.get(settings.default_language, "Persian / Farsi (فارسی)")
norm = lang_code_or_name.lower().strip()
norm = LANG_ALIASES.get(norm, norm)
return AVAILABLE_LANGUAGES.get(norm, lang_code_or_name)
# Global active user input states for interactive prompts (e.g. FTP configuration wizards)
USER_INPUT_STATES: dict[int, dict] = {}
# Logging configuration
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=getattr(logging, settings.log_level.upper(), logging.INFO),
)
logger = logging.getLogger("AGYBot")
# Authorization check decorator/helper
def check_auth(func):
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
if not update.effective_user:
return
user_id = update.effective_user.id
# Allow /start with invite code or /auth command to bypass check_auth
if update.message and update.message.text:
text = update.message.text.strip()
if text.startswith("/start") and len(context.args or []) > 0 and context.args[0].startswith("inv_"):
return await func(update, context, *args, **kwargs)
if text.startswith("/auth"):
return await func(update, context, *args, **kwargs)
if not settings.is_user_authorized(user_id):
session = session_manager.get_or_create(user_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if is_fa:
msg = (
f"⛔ <b>دسترسی به این ربات نیازمند دعوت است!</b>\n\n"
f"این ربات به صورت خصوصی و اختصاصی مدیریت می‌شود و استفاده از امکانات هوش مصنوعی آن نیازمند <b>دعوت یا تایید توسط مدیران سیستم</b> است.\n\n"
f"• 🆔 <b>شناسه عددی شما (User ID):</b> <code>{user_id}</code>\n\n"
f"💡 <i>شما می‌توانید با کلیک روی دکمه زیر، درخواست دسترسی خود را مستقیماً برای ادمین‌ها ارسال کنید:</i>"
)
keyboard = [
[InlineKeyboardButton("📩 ارسال درخواست دسترسی به مدیر", callback_data="req_access")],
]
else:
msg = (
f"⛔ <b>Access Restricted — Invitation Required!</b>\n\n"
f"This bot is private and requires an <b>invitation or approval by an administrator</b>.\n\n"
f"• 🆔 <b>Your Telegram User ID:</b> <code>{user_id}</code>\n\n"
f"💡 <i>Click the button below to send an access request directly to administrators:</i>"
)
keyboard = [
[InlineKeyboardButton("📩 Request Access from Admin", callback_data="req_access")],
]
if update.message:
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif update.callback_query:
data = update.callback_query.data
if data == "req_access":
return await func(update, context, *args, **kwargs)
await update.callback_query.answer("⛔ دسترسی محدود است / Unauthorized", show_alert=True)
return
return await func(update, context, *args, **kwargs)
return wrapper
def build_main_dashboard(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the full interactive glass-button main dashboard."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
if not curr_proj:
if is_fa:
text = (
"👋 <b>به دستیار هوشمند برنامه‌نویسی Antigravity (AGY) خوش آمدید!</b>\n\n"
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n"
"برای شروع گفتگو، کدنویسی و اجرای دستورات هوش مصنوعی، لطفاً ابتدا یک پروژه اختصاصی بسازید.\n\n"
"💡 <i>دستور سریع ساخت پروژه:</i>\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b> <code>/newproject webapp</code>"
)
keyboard = [
[InlineKeyboardButton("➕ ساخت اولین پروژه", callback_data="proj_new")],
[InlineKeyboardButton("📖 راهنمای دستورات", callback_data="btn_help_menu")],
]
else:
text = (
"👋 <b>Welcome to Antigravity (AGY) Assistant!</b>\n\n"
"⚠️ <b>You have not created any projects yet!</b>\n"
"To start chatting and coding with AI, please create your first project.\n\n"
"💡 <i>Quick command:</i>\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Example:</b> <code>/newproject webapp</code>"
)
keyboard = [
[InlineKeyboardButton(" Create First Project", callback_data="proj_new")],
[InlineKeyboardButton("📖 Bot Guide", callback_data="btn_help_menu")],
]
return text, InlineKeyboardMarkup(keyboard)
accessible = session_manager.get_all_accessible_projects(chat_id)
is_owner = (curr_proj.owner_id == chat_id or (curr_proj.name == "default" and is_admin_user))
owner_str = "(مالک: شما)" if is_owner else f"(مالک: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
owner_str_en = "(Owner: You)" if is_owner else f"(Owner: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
ctx_info = f"• 📏 <b>طول کانتکست:</b> <code>{curr_proj.last_context_length:,} توکن</code>\n" if curr_proj.last_context_length else ""
ctx_info_en = f"• 📏 <b>Context Length:</b> <code>{curr_proj.last_context_length:,} tokens</code>\n" if curr_proj.last_context_length else ""
acc_status = auth_manager.get_account_status(chat_id)
acc_badge_fa = "🟢 اختصاصی" if acc_status["has_custom_account"] else "🟡 پیش‌فرض سرور"
acc_badge_en = "🟢 Custom" if acc_status["has_custom_account"] else "🟡 Server Default"
if is_fa:
text = (
f"🎛 <b>کنترل‌پنل مدیریت Antigravity (AGY)</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str}\n"
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🔑 <b>حساب AGY:</b> <b>{acc_badge_fa}</b>\n"
f"• 🧠 <b>مدل هوش مصنوعی:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>زبان پاسخ‌دهی:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 💬 <b>وضعیت مکالمه:</b> <code>{curr_proj.conversation_id or '🟢 نشست تازه (آماده دریافت اولین پیام)'}</code>\n"
f"{ctx_info}"
f"• 📁 <b>تعداد کل پروژه‌ها:</b> <code>{len(accessible)}</code>\n\n"
f"💡 <i>از دکمه‌های شیشه‌ای زیر برای مدیریت آسان و سریع استفاده کنید:</i>"
)
keyboard = [
[
InlineKeyboardButton("📁 پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("📜 گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("⏰ زمان‌بندی", callback_data="btn_tasks_menu"),
],
[
InlineKeyboardButton("🧠 حافظه هوش مصنوعی", callback_data="btn_memory_menu"),
InlineKeyboardButton("📈 سهمیه مصرف", callback_data="btn_usage_menu"),
InlineKeyboardButton("🔑 حساب AGY", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 سخت‌افزار سرور", callback_data="btn_hw_menu"),
InlineKeyboardButton("🐙 گیت (Gitea)", callback_data="btn_git_menu"),
],
[
InlineKeyboardButton("⚙️ تنظیمات و مدل", callback_data="btn_settings_menu"),
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
],
[
InlineKeyboardButton("📖 راهنما", callback_data="btn_help_menu"),
],
]
if is_admin_user:
keyboard.append([
InlineKeyboardButton("👥 مدیریت کاربران و دعوت‌ها", callback_data="btn_users_menu"),
])
else:
text = (
f"🎛 <b>Antigravity (AGY) Control Panel</b>\n\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str_en}\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🔑 <b>AGY Account:</b> <b>{acc_badge_en}</b>\n"
f"• 🧠 <b>AI Model:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>Reasoning Effort:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 💬 <b>Conversation:</b> <code>{curr_proj.conversation_id or '🟢 New Session (Ready)'}</code>\n"
f"{ctx_info_en}"
f"• 📁 <b>Total Accessible Projects:</b> <code>{len(accessible)}</code>\n\n"
f"💡 <i>Use the interactive buttons below to manage your environment:</i>"
)
keyboard = [
[
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("⏰ Schedule", callback_data="btn_tasks_menu"),
],
[
InlineKeyboardButton("🧠 AI Memory", callback_data="btn_memory_menu"),
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
InlineKeyboardButton("🔑 AGY Account", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 Server Hardware", callback_data="btn_hw_menu"),
InlineKeyboardButton("🐙 Git (Gitea)", callback_data="btn_git_menu"),
],
[
InlineKeyboardButton("⚙️ Settings & Model", callback_data="btn_settings_menu"),
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
],
[
InlineKeyboardButton("📖 Bot Guide", callback_data="btn_help_menu"),
],
]
if is_admin_user:
keyboard.append([
InlineKeyboardButton("👥 Users & Invitations", callback_data="btn_users_menu"),
])
return text, InlineKeyboardMarkup(keyboard)
async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive Git / Gitea manager view."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
urls = git_manager.get_repo_urls(curr_proj.name)
status_info = await git_manager.git_status(curr_proj.workspace)
text = format_git_info(
project_name=curr_proj.name,
web_url=urls["web_url"],
clone_url=urls["clone_url"],
branch=status_info.get("branch") or "main",
last_commit=status_info.get("last_commit"),
dirty=status_info.get("dirty", False),
files_count=status_info.get("files_count", 0),
lang=session.language,
)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🌐 مشاهده در مرورگر (Gitea)", url=urls["web_url"]),
],
[
InlineKeyboardButton("🚀 انتشار به پروداکشن (Publish)", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("💻 ریموت SSH", callback_data="btn_ssh_menu"),
InlineKeyboardButton("🚀 دیپلوی FTP", callback_data="btn_ftp_menu"),
],
[
InlineKeyboardButton("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"),
InlineKeyboardButton("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"),
],
[
InlineKeyboardButton("🔄 همگام‌سازی گیت (Sync)", callback_data="btn_git_sync"),
InlineKeyboardButton("💾 ثبت کامیت و پوش (Commit)", callback_data="btn_git_commit"),
],
[
InlineKeyboardButton("⬇️ دریافت تغییرات (Pull)", callback_data="btn_git_pull"),
InlineKeyboardButton("📦 دانلود سورس (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
],
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
keyboard = [
[
InlineKeyboardButton("🌐 Open in Browser (Gitea)", url=urls["web_url"]),
],
[
InlineKeyboardButton("🚀 Publish to Production", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("💻 Remote SSH", callback_data="btn_ssh_menu"),
InlineKeyboardButton("🚀 Deploy via FTP", callback_data="btn_ftp_menu"),
],
[
InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"),
InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"),
],
[
InlineKeyboardButton("🔄 Git Sync", callback_data="btn_git_sync"),
InlineKeyboardButton("💾 Commit & Push", callback_data="btn_git_commit"),
],
[
InlineKeyboardButton("⬇️ Git Pull", callback_data="btn_git_pull"),
InlineKeyboardButton("📦 Download Zip", callback_data=f"proj_backup:{curr_proj.name}"),
],
[
InlineKeyboardButton("📁 Project Manager", callback_data="proj_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
return text, InlineKeyboardMarkup(keyboard)
async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive FTP Deployer and configuration view."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
text = format_ftp_info(
project_name=curr_proj.name,
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user,
path=curr_proj.ftp_path,
tls=curr_proj.ftp_tls,
password=curr_proj.ftp_password,
lang=session.language,
)
tls_toggle_label = f"🔒 پروتکل امن: {'روشن ✅' if curr_proj.ftp_tls else 'خاموش ❌'}" if is_fa else f"🔒 FTPS/TLS: {'ON ✅' if curr_proj.ftp_tls else 'OFF ❌'}"
if is_fa:
keyboard = [
[
InlineKeyboardButton("🚀 دیپلوی نسخه پروداکشن روی FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 تست اتصال FTP", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🌿 انتقال از dev به پروداکشن (Publish)", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ftp_set:host"),
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ftp_set:user"),
],
[
InlineKeyboardButton("🔑 ویرایش رمز عبور", callback_data="ftp_set:pass"),
InlineKeyboardButton("📂 ویرایش مسیر مقصد", callback_data="ftp_set:path"),
],
[
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
InlineKeyboardButton("🗑️ پاک‌سازی اطلاعات", callback_data="ftp_clear_conf"),
],
[
InlineKeyboardButton("🐙 منوی گیت و برنچ‌ها", callback_data="btn_git_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
keyboard = [
[
InlineKeyboardButton("🚀 Deploy Production to FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 Test FTP Connection", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🌿 Publish dev to Production", callback_data="btn_git_publish"),
],
[
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ftp_set:host"),
InlineKeyboardButton("👤 Edit Username", callback_data="ftp_set:user"),
],
[
InlineKeyboardButton("🔑 Edit Password", callback_data="ftp_set:pass"),
InlineKeyboardButton("📂 Edit Remote Path", callback_data="ftp_set:path"),
],
[
InlineKeyboardButton(tls_toggle_label, callback_data="ftp_toggle_tls"),
InlineKeyboardButton("🗑️ Clear Config", callback_data="ftp_clear_conf"),
],
[
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
return text, InlineKeyboardMarkup(keyboard)
async def build_ssh_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive SSH Remote Dev and deployment view."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
text = format_ssh_info(
project_name=curr_proj.name,
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user,
path=curr_proj.ssh_path,
password=curr_proj.ssh_password,
has_key=bool(curr_proj.ssh_key),
lang=session.language,
)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🚀 ارسال به پروداکشن (Push)", callback_data="btn_ssh_push"),
InlineKeyboardButton("📥 دریافت از سرور (Pull)", callback_data="btn_ssh_pull"),
],
[
InlineKeyboardButton("🔍 تست اتصال SSH", callback_data="btn_ssh_test"),
InlineKeyboardButton("💻 اجرای فرمان ریموت", callback_data="btn_ssh_exec_prompt"),
],
[
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ssh_set:host"),
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ssh_set:user"),
],
[
InlineKeyboardButton("🔑 ویرایش رمز عبور", callback_data="ssh_set:pass"),
InlineKeyboardButton("🗝️ تنظیم کلید SSH Key", callback_data="ssh_set:key"),
],
[
InlineKeyboardButton("📂 ویرایش مسیر ریموت", callback_data="ssh_set:path"),
InlineKeyboardButton("📜 تاریخچه عملیات", callback_data="btn_ssh_logs"),
],
[
InlineKeyboardButton("🗑️ پاک‌سازی اطلاعات", callback_data="ssh_clear_conf"),
InlineKeyboardButton("🐙 منوی گیت و برنچ‌ها", callback_data="btn_git_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
keyboard = [
[
InlineKeyboardButton("🚀 Push to Production", callback_data="btn_ssh_push"),
InlineKeyboardButton("📥 Pull from Server", callback_data="btn_ssh_pull"),
],
[
InlineKeyboardButton("🔍 Test SSH Connection", callback_data="btn_ssh_test"),
InlineKeyboardButton("💻 Run Remote Command", callback_data="btn_ssh_exec_prompt"),
],
[
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ssh_set:host"),
InlineKeyboardButton("👤 Edit Username", callback_data="ssh_set:user"),
],
[
InlineKeyboardButton("🔑 Edit Password", callback_data="ssh_set:pass"),
InlineKeyboardButton("🗝️ Set SSH Key", callback_data="ssh_set:key"),
],
[
InlineKeyboardButton("📂 Edit Remote Path", callback_data="ssh_set:path"),
InlineKeyboardButton("📜 Audit Logs", callback_data="btn_ssh_logs"),
],
[
InlineKeyboardButton("🗑️ Clear Config", callback_data="ssh_clear_conf"),
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
return text, InlineKeyboardMarkup(keyboard)
async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]:
"""Generates paginated commit history view."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
status_info = await git_manager.git_status(curr_proj.workspace)
branch = status_info.get("branch") or "main"
all_commits = await git_manager.get_commit_history(curr_proj.workspace, limit=20)
per_page = 5
total_commits = len(all_commits)
total_pages = max(1, (total_commits + per_page - 1) // per_page)
current_page = max(1, min(page, total_pages))
start_idx = (current_page - 1) * per_page
page_commits = all_commits[start_idx:start_idx + per_page]
text = format_git_commits_page(
project_name=curr_proj.name,
commits=page_commits,
current_page=current_page,
total_pages=total_pages,
branch=branch,
lang=session.language,
)
keyboard = []
for c in page_commits:
short_h = c.get("short_hash", "")
subj = c.get("subject", "")
if len(subj) > 28:
subj = subj[:25] + "..."
is_curr = c.get("is_current", False)
prefix = "📍" if is_curr else "🔖"
btn_text = f"{prefix} {short_h}: {subj}"
keyboard.append([InlineKeyboardButton(btn_text, callback_data=f"git_view:{short_h}")])
nav_row = []
if current_page > 1:
nav_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"git_hist:{current_page - 1}"))
nav_row.append(InlineKeyboardButton(f"📄 {current_page}/{total_pages}", callback_data=f"git_hist:{current_page}"))
if current_page < total_pages:
nav_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"git_hist:{current_page + 1}"))
if nav_row:
keyboard.append(nav_row)
action_row = []
if branch != "main":
action_row.append(InlineKeyboardButton("🌿 برگشت به main" if is_fa else "🌿 Return to main", callback_data="git_co_main"))
action_row.append(InlineKeyboardButton("🔄 بروزرسانی" if is_fa else "🔄 Refresh", callback_data=f"git_hist:{current_page}"))
keyboard.append(action_row)
keyboard.append([
InlineKeyboardButton("🔙 بازگشت به منوی گیت" if is_fa else "🔙 Back to Git Menu", callback_data="btn_git_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(keyboard)
async def build_git_commit_detail_menu(chat_id: int, commit_hash: str) -> tuple[str, InlineKeyboardMarkup]:
"""Generates view for a single commit details with action buttons."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ پروژه‌ای یافت نشد." if is_fa else "⚠️ No project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")]])
detail = await git_manager.get_commit_detail(curr_proj.workspace, commit_hash)
if "error" in detail:
err_msg = f"⚠️ خطا در بارگذاری کامیت: {detail['error']}" if is_fa else f"⚠️ Error loading commit: {detail['error']}"
return err_msg, InlineKeyboardMarkup([[InlineKeyboardButton("🔙 بازگشت به تاریخچه", callback_data="git_hist:1")]])
urls = git_manager.get_repo_urls(curr_proj.name)
text = format_commit_detail_view(
project_name=curr_proj.name,
commit_detail=detail,
web_url=urls["web_url"],
lang=session.language,
)
short_h = detail.get("short_hash", commit_hash)
is_curr = detail.get("is_current", False)
keyboard = []
if not is_curr:
keyboard.append([
InlineKeyboardButton("🔀 سوییچ به این کامیت (Checkout)" if is_fa else "🔀 Switch to this commit (Checkout)", callback_data=f"git_co:{short_h}"),
InlineKeyboardButton("↩️ لغو این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
])
keyboard.append([
InlineKeyboardButton("⏪ بازگردانی پروژه به این کامیت (Reset)" if is_fa else "⏪ Restore Project to this Commit (Reset)", callback_data=f"git_ask_reset:{short_h}"),
])
else:
keyboard.append([
InlineKeyboardButton("↩️ لغو تغییرات این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
InlineKeyboardButton("✅ کامیت فعال فعلی" if is_fa else "✅ Currently Active Commit", callback_data=f"git_view:{short_h}"),
])
keyboard.append([
InlineKeyboardButton("🌿 سوییچ به شاخه اصلی (main)" if is_fa else "🌿 Switch to main", callback_data="git_co_main"),
])
keyboard.append([
InlineKeyboardButton("📜 لیست کامیت‌ها" if is_fa else "📜 Commit List", callback_data="git_hist:1"),
InlineKeyboardButton("🔙 منوی گیت" if is_fa else "🔙 Git Menu", callback_data="btn_git_menu"),
])
return text, InlineKeyboardMarkup(keyboard)
def build_projects_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive Project Manager text and keyboard markup."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_admin_user = settings.is_admin(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
own_projs = session_manager.get_user_projects(chat_id)
shared_projs = session_manager.get_shared_projects(chat_id)
accessible = session_manager.get_all_accessible_projects(chat_id)
if not accessible or not curr_proj:
if is_fa:
text = (
"📁 <b>مدیریت پروژه‌ها (Project Manager)</b>\n\n"
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n"
"برای شروع گفتگو و کدنویسی با هوش مصنوعی، لطفاً ابتدا یک پروژه اختصاصی بسازید.\n\n"
"💡 <i>دستور ایجاد سریع پروژه:</i>\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b> <code>/newproject webapp</code>"
)
keyboard = [
[InlineKeyboardButton("➕ ساخت پروژه جدید", callback_data="proj_new")],
[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")],
]
else:
text = (
"📁 <b>Project Manager</b>\n\n"
"⚠️ <b>You haven't created any projects yet!</b>\n"
"To start chatting and coding with AI, please create your first project.\n\n"
"💡 <i>Create command:</i>\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Example:</b> <code>/newproject webapp</code>"
)
keyboard = [
[InlineKeyboardButton(" Create Project", callback_data="proj_new")],
[InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard")],
]
return text, InlineKeyboardMarkup(keyboard)
total_projs = len(accessible)
ctx_info = f"• <b>کانتکست:</b> <code>{curr_proj.last_context_length:,} توکن</code>\n" if curr_proj.last_context_length else ""
is_owner = (curr_proj.owner_id == chat_id or (curr_proj.name == "default" and is_admin_user))
owner_str = "(مالک: شما)" if is_owner else f"(مالک: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
owner_str_en = "(Owner: You)" if is_owner else f"(Owner: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
if is_fa:
text = (
f"📁 <b>مدیریت پروژه‌ها (Project Manager)</b>\n\n"
f"🟢 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str}\n"
f"📂 <b>مسیر:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"🧠 <b>مدل:</b> <code>{escape_html(curr_proj.model)}</code>\n"
f"⚡ <b>استدلال:</b> <code>{escape_html(curr_proj.effort)}</code>\n"
f"💬 <b>گفتگو:</b> <code>{curr_proj.conversation_id or '🟢 نشست تازه (آماده دریافت پیام)'}</code>\n"
f"{ctx_info}"
)
if curr_proj.shared_with and is_owner:
shared_str = ", ".join([f"<code>{uid}</code>" for uid in curr_proj.shared_with])
text += f"👥 <b>اشتراک با ({len(curr_proj.shared_with)} کاربر):</b> {shared_str}\n"
text += "──────────────\n"
if own_projs:
text += f"📋 <b>پروژه‌های اختصاصی شما ({len(own_projs)}):</b>\n"
for name, p in own_projs.items():
active_marker = " 🟢 <i>[فعال]</i>" if name == session.active_project else ""
shared_badge = f" 🤝<i>({len(p.shared_with)})</i>" if p.shared_with else ""
text += f"• <b>{escape_html(name)}</b>: <code>{escape_html(p.workspace)}</code>{shared_badge}{active_marker}\n"
if shared_projs:
text += f"\n👥 <b>پروژه‌های اشتراکی با شما ({len(shared_projs)}):</b>\n"
for name, p in shared_projs.items():
active_marker = " 🟢 <i>[فعال]</i>" if (name == session.active_project or f"{name} (shared:{p.owner_id})" == session.active_project) else ""
text += f"• <b>{escape_html(p.name)}</b> (مالک: <code>{p.owner_id}</code>){active_marker}\n"
text += (
f"\n💡 <i>ایجاد پروژه:</i> <code>/newproject &lt;نام&gt;</code>\n"
f"<i>اشتراک‌گذاری:</i> <code>/share &lt;user_id&gt;</code>\n"
f"<i>سوییچ سریع:</i> <code>/switch &lt;نام&gt;</code>"
)
else:
text = (
f"📁 <b>Project Manager</b>\n\n"
f"🟢 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str_en}\n"
f"📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"🧠 <b>Model:</b> <code>{escape_html(curr_proj.model)}</code>\n"
f"⚡ <b>Effort:</b> <code>{escape_html(curr_proj.effort)}</code>\n"
f"💬 <b>Conversation:</b> <code>{curr_proj.conversation_id or 'New (Unstarted)'}</code>\n"
f"{ctx_info}"
)
if curr_proj.shared_with and is_owner:
shared_str = ", ".join([f"<code>{uid}</code>" for uid in curr_proj.shared_with])
text += f"👥 <b>Shared with:</b> {shared_str}\n"
text += "──────────────\n"
if own_projs:
text += f"📋 <b>Your Projects ({len(own_projs)}):</b>\n"
for name, p in own_projs.items():
active_marker = " 🟢 <i>[Active]</i>" if name == session.active_project else ""
shared_badge = f" 🤝<i>({len(p.shared_with)})</i>" if p.shared_with else ""
text += f"• <b>{escape_html(name)}</b>: <code>{escape_html(p.workspace)}</code>{shared_badge}{active_marker}\n"
if shared_projs:
text += f"\n👥 <b>Shared with You ({len(shared_projs)}):</b>\n"
for name, p in shared_projs.items():
active_marker = " 🟢 <i>[Active]</i>" if (name == session.active_project or f"{name} (shared:{p.owner_id})" == session.active_project) else ""
text += f"• <b>{escape_html(p.name)}</b> (Owner: <code>{p.owner_id}</code>){active_marker}\n"
text += (
f"\n💡 <i>Create:</i> <code>/newproject &lt;name&gt;</code>\n"
f"<i>Share:</i> <code>/share &lt;user_id&gt;</code>\n"
f"<i>Quick switch:</i> <code>/switch &lt;name&gt;</code>"
)
# Build keyboard
keyboard = []
proj_row = []
for key, p in accessible.items():
is_active = (key == session.active_project or p.name == session.active_project)
is_shared = (p.owner_id != chat_id and p.name != "default")
icon = "✅" if is_active else ("👥" if is_shared else "📁")
btn_label = f"{icon} {p.name}"
proj_row.append(InlineKeyboardButton(btn_label, callback_data=f"proj_switch:{key}"))
if len(proj_row) == 2:
keyboard.append(proj_row)
proj_row = []
if proj_row:
keyboard.append(proj_row)
if is_fa:
keyboard.append([
InlineKeyboardButton("➕ پروژه جدید", callback_data="proj_new"),
InlineKeyboardButton("📤 آپلود فایل (وب)", callback_data=f"proj_upload_link:{curr_proj.name}"),
])
keyboard.append([
InlineKeyboardButton("📦 دریافت فایل بکاپ (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 اشتراک‌گذاری", callback_data="proj_share_menu"),
])
keyboard.append([
InlineKeyboardButton("🧠 حافظه پروژه", callback_data="mem_tab:project:0"),
InlineKeyboardButton("📂 مسیر کاری", callback_data="proj_ws_info"),
])
keyboard.append([
InlineKeyboardButton("🗑️ حذف پروژه", callback_data="proj_del_menu"),
InlineKeyboardButton("🧠 انتخاب مدل", callback_data="proj_model_menu"),
])
keyboard.append([
InlineKeyboardButton("🔄 ری‌استارت گفتگو", callback_data="btn_restart"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
])
keyboard.append([
InlineKeyboardButton("🔙 بستن منو", callback_data="proj_close"),
])
else:
keyboard.append([
InlineKeyboardButton(" New Project", callback_data="proj_new"),
InlineKeyboardButton("📤 Web Upload", callback_data=f"proj_upload_link:{curr_proj.name}"),
])
keyboard.append([
InlineKeyboardButton("📦 Backup Project (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 Share Project", callback_data="proj_share_menu"),
])
keyboard.append([
InlineKeyboardButton("🧠 Project Memory", callback_data="mem_tab:project:0"),
InlineKeyboardButton("📂 Workspace Info", callback_data="proj_ws_info"),
])
keyboard.append([
InlineKeyboardButton("🗑️ Delete Project", callback_data="proj_del_menu"),
InlineKeyboardButton("🧠 Switch Model", callback_data="proj_model_menu"),
])
keyboard.append([
InlineKeyboardButton("🔄 Reset Chat", callback_data="btn_restart"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
])
keyboard.append([
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
])
return text, InlineKeyboardMarkup(keyboard)
def build_sharing_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive Project Sharing text and keyboard markup."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_admin_user = settings.is_admin(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
return build_projects_menu(chat_id)
is_owner = (curr_proj.owner_id == chat_id or (curr_proj.name == "default" and is_admin_user))
if is_fa:
text = (
f"🤝 <b>مدیریت اشتراک‌گذاری پروژه (Project Sharing)</b>\n\n"
f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"👑 <b>مالک:</b> <code>{curr_proj.owner_id or 'سیستم'}</code> {'(شما)' if is_owner else ''}\n"
f"📂 <b>مسیر:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
)
if not is_owner and not is_admin_user:
text += (
"⚠️ <i>شما مالک این پروژه نیستید و امکان مدیریت دسترسی‌های آن را ندارید.</i>\n\n"
"برای اشتراک‌گذاری، ابتدا به یکی از پروژه‌های شخصی خود سوییچ نمایید."
)
keyboard = [
[InlineKeyboardButton("📁 بازگشت به پروژه‌ها", callback_data="proj_menu")],
[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")],
]
return text, InlineKeyboardMarkup(keyboard)
if curr_proj.shared_with:
text += f"👥 <b>کاربران دارای دسترسی ({len(curr_proj.shared_with)}):</b>\n"
for uid in curr_proj.shared_with:
text += f"• <code>{uid}</code>\n"
else:
text += "👥 <b>کاربران دارای دسترسی:</b> <i>هیچ کاربری اضافه نشده است (فقط شما).</i>\n"
text += (
"\n──────────────\n"
"💡 <b>نحوه اشتراک‌گذاری با کاربر دیگر:</b>\n"
"دستور زیر را در چت ارسال کنید:\n"
f"<code>/share &lt;شناسه_کاربری&gt;</code>\n\n"
"<b>مثال:</b> <code>/share 123456789</code>\n\n"
"💡 <b>لغو اشتراک با یک کاربر:</b>\n"
"<code>/unshare &lt;شناسه_کاربری&gt;</code>"
)
else:
text = (
f"🤝 <b>Project Sharing Management</b>\n\n"
f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"👑 <b>Owner:</b> <code>{curr_proj.owner_id or 'System'}</code> {'(You)' if is_owner else ''}\n"
f"📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
)
if not is_owner and not is_admin_user:
text += (
"⚠️ <i>You are not the owner of this project and cannot manage its permissions.</i>\n\n"
"Switch to one of your own projects to share it."
)
keyboard = [
[InlineKeyboardButton("📁 Back to Projects", callback_data="proj_menu")],
[InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard")],
]
return text, InlineKeyboardMarkup(keyboard)
if curr_proj.shared_with:
text += f"👥 <b>Authorized Users ({len(curr_proj.shared_with)}):</b>\n"
for uid in curr_proj.shared_with:
text += f"• <code>{uid}</code>\n"
else:
text += "👥 <b>Authorized Users:</b> <i>No users added (only you).</i>\n"
text += (
"\n──────────────\n"
"💡 <b>To share with a user:</b>\n"
"Send command in chat:\n"
f"<code>/share &lt;user_id&gt;</code>\n\n"
"<b>Example:</b> <code>/share 123456789</code>\n\n"
"💡 <b>To revoke access:</b>\n"
"<code>/unshare &lt;user_id&gt;</code>"
)
keyboard = []
if is_owner or is_admin_user:
for uid in curr_proj.shared_with:
lbl = f"❌ لغو اشتراک {uid}" if is_fa else f"❌ Revoke {uid}"
keyboard.append([InlineKeyboardButton(lbl, callback_data=f"proj_unshare_confirm:{uid}")])
keyboard.append([
InlineKeyboardButton("📁 بازگشت به پروژه‌ها" if is_fa else "📁 Back to Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(keyboard)
def build_settings_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive Settings & AI Configuration submenu."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
text = "⚠️ هیچ پروژه‌ای فعال نیست." if is_fa else "⚠️ No active project."
keyboard = [[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]]
return text, InlineKeyboardMarkup(keyboard)
if is_fa:
text = (
f"⚙️ <b>تنظیمات پروژه و هوش مصنوعی</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل هوش مصنوعی:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>زبان پاسخ‌دهی:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>از گزینه‌های زیر برای پیکربندی استفاده کنید:</i>"
)
keyboard = [
[
InlineKeyboardButton("🧠 انتخاب مدل هوش مصنوعی", callback_data="proj_model_menu"),
],
[
InlineKeyboardButton("⚡ سطح تفکر / استدلال (Effort)", callback_data="btn_effort_menu"),
],
[
InlineKeyboardButton("🧠 مدیریت حافظه هوش مصنوعی", callback_data="btn_memory_menu"),
],
[
InlineKeyboardButton("🌐 زبان پاسخ‌دهی", callback_data="btn_lang_menu"),
InlineKeyboardButton("📊 وضعیت و کانتکست", callback_data="btn_status_menu"),
],
[
InlineKeyboardButton("🔑 حساب کاربری AGY (لاگین گوگل)", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("📦 دریافت فایل بکاپ (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 اشتراک‌گذاری پروژه", callback_data="proj_share_menu"),
],
[
InlineKeyboardButton("🏠 بازگشت به منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"⚙️ <b>Project & AI Settings</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>AI Model:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>Reasoning Effort:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>Select an option below to configure:</i>"
)
keyboard = [
[
InlineKeyboardButton("🧠 Select AI Model", callback_data="proj_model_menu"),
],
[
InlineKeyboardButton("⚡ Reasoning Effort", callback_data="btn_effort_menu"),
],
[
InlineKeyboardButton("🧠 AI Memory Management", callback_data="btn_memory_menu"),
],
[
InlineKeyboardButton("🌐 Response Language", callback_data="btn_lang_menu"),
InlineKeyboardButton("📊 Status & Tokens", callback_data="btn_status_menu"),
],
[
InlineKeyboardButton("🔑 AGY Account & Login", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("📦 Project Backup (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 Share Project", callback_data="proj_share_menu"),
],
[
InlineKeyboardButton("🏠 Back to Main Dashboard", callback_data="btn_dashboard"),
],
]
return text, InlineKeyboardMarkup(keyboard)
def build_account_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive AGY Account Management submenu."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
status = auth_manager.get_account_status(chat_id)
has_custom = status["has_custom_account"]
login_in_progress = status["is_login_in_progress"]
if is_fa:
if has_custom:
acc_type_badge = "🟢 <b>حساب اختصاصی فعال (Custom Account)</b>"
desc = (
"درخواست‌ها و مصرف مدل‌های هوش مصنوعی شما مستقیماً از <b>حساب گوگل اختصاصی خودتان</b> کسر می‌شود.\n"
f"• 📅 <i>تاریخ ثبت/بروزرسانی توکن:</i> <code>{status['last_modified']}</code>\n"
)
else:
acc_type_badge = "🔴 <b>حساب متصل نیست (Disconnected)</b>"
desc = (
"⚠️ <b>هیچ حساب کاربری فعالی متصل نشده است.</b>\n"
"در این ربات حساب‌های عمومی/اشتراکی غیرفعال هستند و برای استفاده از هوش مصنوعی و ارسال دستورات، اتصال حساب اختصاصی گوگل الزامی است.\n"
)
text = (
f"🔑 <b>مدیریت حساب کاربری هوش مصنوعی (AGY Account)</b>\n\n"
f"• 🆔 <b>شناسه کاربر:</b> <code>{chat_id}</code>\n"
f"• 👤 <b>وضعیت حساب:</b> {acc_type_badge}\n\n"
f"{desc}\n"
f"💡 <i>روش‌های اتصال حساب اختصاصی:</i>\n"
f"۱. <b>ورود اینتراکتیو با گوگل:</b> دکمه «🔑 ورود با حساب گوگل (OAuth)» را بزنید.\n"
f"۲. <b>ارسال مستقیم فایل یا متن توکن:</b> فایل <code>antigravity-oauth-token</code> یا متن JSON توکن خود را در چت ارسال کنید."
)
buttons = []
if login_in_progress:
buttons.append([
InlineKeyboardButton("❌ لغو فرآیند لاگین جاری", callback_data="btn_account_cancel_login"),
])
else:
buttons.append([
InlineKeyboardButton("🔑 ورود با حساب گوگل (OAuth)", callback_data="btn_account_start_login"),
])
if has_custom:
buttons.append([
InlineKeyboardButton("🚪 خروج از حساب شخصی", callback_data="btn_account_logout"),
])
buttons.append([
InlineKeyboardButton("📊 استعلام سهمیه حساب", callback_data="btn_usage_menu"),
InlineKeyboardButton("⚙️ تنظیمات", callback_data="btn_settings_menu"),
])
buttons.append([
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
])
else:
if has_custom:
acc_type_badge = "🟢 <b>Active Custom Account</b>"
desc = f"All AI interactions are using your personal Google account quota.\n• 📅 <i>Last modified:</i> <code>{status['last_modified']}</code>\n"
else:
acc_type_badge = "🔴 <b>No Account Connected</b>"
desc = "Shared accounts are disabled. You must log in with your own Google account to use the bot.\n"
text = (
f"🔑 <b>AGY Account Management</b>\n\n"
f"• 🆔 <b>User ID:</b> <code>{chat_id}</code>\n"
f"• 👤 <b>Account Status:</b> {acc_type_badge}\n\n"
f"{desc}\n"
f"💡 <i>Login options:</i>\n"
f"1. <b>Google OAuth:</b> Tap 'Log in with Google' button below.\n"
f"2. <b>Direct Token:</b> Upload your <code>antigravity-oauth-token</code> file or JSON text."
)
buttons = []
if login_in_progress:
buttons.append([InlineKeyboardButton("❌ Cancel Pending Login", callback_data="btn_account_cancel_login")])
else:
buttons.append([InlineKeyboardButton("🔑 Log in with Google", callback_data="btn_account_start_login")])
if has_custom:
buttons.append([InlineKeyboardButton("🚪 Log out from Custom Account", callback_data="btn_account_logout")])
buttons.append([
InlineKeyboardButton("📊 Check Quota", callback_data="btn_usage_menu"),
InlineKeyboardButton("⚙️ Settings", callback_data="btn_settings_menu"),
])
buttons.append([
InlineKeyboardButton("🏠 Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(buttons)
async def build_usage_report(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Queries AGY CLI /usage and /credits and generates an interactive report with visual progress bars."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
text = await fetch_and_render_usage_report(is_fa=is_fa, user_id=chat_id)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🔄 بروزرسانی مصرف", callback_data="btn_usage_refresh"),
InlineKeyboardButton("🔑 مدیریت حساب AGY", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 سخت‌افزار سرور", callback_data="btn_hw_menu"),
InlineKeyboardButton("📁 پروژه‌ها", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 بستن", callback_data="proj_close"),
],
]
else:
keyboard = [
[
InlineKeyboardButton("🔄 Refresh Quota", callback_data="btn_usage_refresh"),
InlineKeyboardButton("🔑 AGY Account", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 Server Hardware", callback_data="btn_hw_menu"),
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
],
]
return text, InlineKeyboardMarkup(keyboard)
def build_conversations_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive Conversation History list and switcher/manager keyboard."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
text = "⚠️ هیچ پروژه‌ای یافت نشد." if is_fa else "⚠️ No active project found."
return text, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
convs = session_manager.get_project_conversations(chat_id)
if is_fa:
header = (
f"📜 <b>مدیریت و تاریخچه گفتگوها (پروژه: {escape_html(curr_proj.name)})</b>\n\n"
f"💡 <i>جهت سوییچ یا مشاهده جزئیات و حذف هر گفتگو، روی دکمه مربوطه کلیک کنید:</i>\n\n"
)
else:
header = (
f"📜 <b>Conversation Manager & History (Project: {escape_html(curr_proj.name)})</b>\n\n"
f"💡 <i>Click any conversation button to switch, view details, or delete:</i>\n\n"
)
if not convs:
empty_str = "<i>هنوز گفتگویی در این پروژه ثبت نشده است. با ارسال اولین پیام، گفتگو ساخته می‌شود.</i>" if is_fa else "<i>No conversations recorded yet. Send a message to start one.</i>"
keyboard = [
[
InlineKeyboardButton("🔄 گفتگوی جدید" if is_fa else "🔄 New Chat", callback_data="btn_restart"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
]
]
return header + empty_str, InlineKeyboardMarkup(keyboard)
body_lines = []
keyboard = []
for idx, c in enumerate(convs[:8], start=1):
cid = c["id"]
is_curr = c.get("is_current", False)
badge_text = "🟢 [فعال]" if is_curr else f"#{idx}"
custom_t = c.get("title")
prompt_snippet = custom_t if custom_t else (c.get("first_prompt") or "(بدون متن)")
prompt_clean = prompt_snippet.replace("\n", " ").strip()
# Format text preview for message body (up to 80 chars)
prompt_body_preview = prompt_clean if len(prompt_clean) <= 80 else prompt_clean[:77] + "..."
# Format label for full-width button (up to 45 chars)
prompt_btn_preview = prompt_clean if len(prompt_clean) <= 45 else prompt_clean[:42] + "..."
turns = c.get("turns_count", 0)
title_prefix = "🏷️ " if custom_t else ""
if is_fa:
status_tag = " <b>(در حال استفاده)</b>" if is_curr else ""
body_lines.append(f"<b>{idx}.</b> {badge_text}{status_tag} <code>{cid[:8]}</code> ({turns} نوبت)\n{title_prefix}<i>«{escape_html(prompt_body_preview)}»</i>\n")
else:
status_tag = " <b>(Active)</b>" if is_curr else ""
body_lines.append(f"<b>{idx}.</b> {badge_text}{status_tag} <code>{cid[:8]}</code> ({turns} turns)\n{title_prefix}<i>\"{escape_html(prompt_body_preview)}\"</i>\n")
if is_curr:
btn_label = f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (فعال)" if is_fa else f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (Active)"
btn_action = f"conv_view_{cid}"
else:
btn_label = f"💬 #{idx}: {title_prefix}{prompt_btn_preview}"
btn_action = f"conv_switch_{cid}"
# Full width button for maximum readability
keyboard.append([
InlineKeyboardButton(btn_label[:60], callback_data=btn_action),
])
action_row = [
InlineKeyboardButton("⏮️ آخرین گفتگو" if is_fa else "⏮️ Last Chat", callback_data="btn_conv_last"),
InlineKeyboardButton("🔄 گفتگوی جدید" if is_fa else "🔄 New Chat", callback_data="btn_restart"),
]
keyboard.append(action_row)
if convs:
keyboard.append([
InlineKeyboardButton("🗑️ پاکسازی تمام گفتگوها" if is_fa else "🗑️ Clear All Conversations", callback_data="conv_clear_all_ask"),
])
nav_row = [
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
]
keyboard.append(nav_row)
if is_fa:
footer = (
f"\n💡 <i>دستورات متنی:</i>\n"
f"• سوییچ: <code>/switchconv &lt;شماره یا شناسه&gt;</code>\n"
f"• تغییر موضوع: <code>/settopic &lt;عنوان جدید&gt;</code>\n"
f"• حذف: <code>/delconv &lt;شماره یا شناسه&gt;</code>\n"
f"• پاکسازی همه: <code>/clearconvs</code>"
)
else:
footer = (
f"\n💡 <i>Text commands:</i>\n"
f"• Switch: <code>/switchconv &lt;number or ID&gt;</code>\n"
f"• Rename: <code>/settopic &lt;new title&gt;</code>\n"
f"• Delete: <code>/delconv &lt;number or ID&gt;</code>\n"
f"• Clear all: <code>/clearconvs</code>"
)
return header + "".join(body_lines) + footer, InlineKeyboardMarkup(keyboard)
def build_conversation_detail_menu(chat_id: int, conv_id: str) -> tuple[str, InlineKeyboardMarkup]:
"""Builds a detailed view of a single conversation with switch and delete options."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
text = "⚠️ هیچ پروژه‌ای یافت نشد." if is_fa else "⚠️ No active project found."
return text, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
meta = get_conversation_metadata(conv_id)
out = get_conversation_last_output(conv_id)
is_active = (curr_proj.conversation_id == conv_id)
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
custom_title = getattr(curr_proj, "conversation_titles", {}).get(conv_id) if hasattr(curr_proj, "conversation_titles") else None
display_title = custom_title if custom_title else first_prompt
last_resp = out.get("text") or meta.get("last_response") or ""
if len(last_resp) > 200:
last_resp = last_resp[:197] + "..."
status_str = "🟢 فعال (در حال استفاده)" if is_active else "⚪ غیرفعال (بایگانی شده)"
status_str_en = "🟢 Active (In use)" if is_active else "⚪ Inactive (Archived)"
if is_fa:
title_section = f"• 🏷️ <b>عنوان / موضوع گفتگو:</b> <b>{escape_html(custom_title)}</b>\n• 📝 <b>اولین پیام:</b> <i>«{escape_html(first_prompt[:200])}»</i>\n" if custom_title else f"📝 <b>موضوع / اولین پیام:</b>\n<i>«{escape_html(first_prompt[:250])}»</i>\n"
text = (
f"🔍 <b>جزئیات گفتگو (Conversation Details)</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>شناسه گفتگو:</b> <code>{conv_id}</code>\n"
f"• 🏷 <b>وضعیت:</b> {status_str}\n"
f"• 🔢 <b>تعداد نوبت‌ها:</b> <code>{meta.get('turns_count', 0)} نوبت</code>\n\n"
f"{title_section}"
)
if last_resp:
text += f"\n📋 <b>آخرین پاسخ AI:</b>\n<i>«{escape_html(last_resp)}»</i>\n"
else:
title_section = f"• 🏷️ <b>Topic / Title:</b> <b>{escape_html(custom_title)}</b>\n• 📝 <b>First Prompt:</b> <i>\"{escape_html(first_prompt[:200])}\"</i>\n" if custom_title else f"📝 <b>First Prompt / Topic:</b>\n<i>\"{escape_html(first_prompt[:250])}\"</i>\n"
text = (
f"🔍 <b>Conversation Details</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Conversation ID:</b> <code>{conv_id}</code>\n"
f"• 🏷 <b>Status:</b> {status_str_en}\n"
f"• 🔢 <b>Turns:</b> <code>{meta.get('turns_count', 0)}</code>\n\n"
f"{title_section}"
)
if last_resp:
text += f"\n📋 <b>Last AI Response:</b>\n<i>\"{escape_html(last_resp)}\"</i>\n"
keyboard = []
if not is_active:
keyboard.append([
InlineKeyboardButton("🟢 سوییچ و فعال‌سازی این گفتگو" if is_fa else "🟢 Switch & Resume Chat", callback_data=f"conv_switch_{conv_id}"),
])
keyboard.append([
InlineKeyboardButton("🗑️ حذف این گفتگو" if is_fa else "🗑️ Delete this Conversation", callback_data=f"conv_del_ask_{conv_id}"),
])
keyboard.append([
InlineKeyboardButton("📜 بازگشت به لیست گفتگوها" if is_fa else "📜 Back to Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(keyboard)
def process_ai_schedule_actions(raw_text: str, chat_id: int, project_name: str, is_fa: bool) -> tuple[str, list[ScheduledTask]]:
"""
Scans AI response for schedule action tags:
[[SCHEDULE_TASK: timing="...", max_runs=..., type="...", title="...", content="..."]]
or
[[SCHEDULE: timing="...", max_runs=..., type="...", title="...", content="..."]]
Creates the tasks in task_scheduler and replaces the raw tags with formatted Telegram confirmation cards.
"""
created_tasks = []
pattern = r"\[\[(?:SCHEDULE_TASK|SCHEDULE|CREATE_TASK):\s*(.*?)\]\]"
def replacer(match):
attrs_str = match.group(1)
timing = ""
content = ""
title = ""
task_type = "prompt"
max_runs = None
kv_pairs = re.findall(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s,\]]+))', attrs_str)
for k, v1, v2, v3 in kv_pairs:
key = k.lower()
val = v1 if v1 != "" else (v2 if v2 != "" else v3)
if key == "timing":
timing = val
elif key in ("content", "prompt", "cmd", "command"):
content = val
elif key == "title":
title = val
elif key == "type":
task_type = val.lower()
elif key in ("max_runs", "repeat", "count", "times", "max_iterations"):
try:
if val and val.lower() not in ("none", "null", "unlimited", "0"):
max_runs = int(val)
except ValueError:
pass
if not timing or not content:
return ""
try:
task = task_scheduler.add_task(
chat_id=chat_id,
creator_id=chat_id,
project_name=project_name,
task_type=task_type,
content=content,
timing_spec=timing,
title=title,
max_runs=max_runs,
)
created_tasks.append(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 "نامحدود (دائمی)"
if is_fa:
card = (
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>"
)
else:
card = (
f"\n\n⏰ <b>Scheduled Task Registered by AI:</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 card
except Exception as e:
logger.error(f"Failed to auto-register AI schedule task: {e}")
return f"\n⚠️ <i>(خطا در ثبت زمان‌بندی: {escape_html(str(e))})</i>"
processed_text = re.sub(pattern, replacer, raw_text, flags=re.DOTALL)
return processed_text, created_tasks
def build_memory_menu(chat_id: int, view_type: str = "project", page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive 3-tier AI Memory manager dashboard with tabs (project, user, global), pagination, and deletion."""
from memory_manager import memory_manager
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
proj_name = curr_proj.name if curr_proj else "default"
# Non-admin users are strictly forbidden from viewing global memory
if view_type == "global" and not is_admin_user:
view_type = "project" if curr_proj else "user"
# Validate view_type
if view_type not in ("project", "user", "global"):
view_type = "project" if curr_proj else "user"
if view_type == "project":
memories = memory_manager.get_project_memories(user_id=chat_id, project_name=proj_name, limit=100)
elif view_type == "user":
memories = memory_manager.get_user_memories(user_id=chat_id, limit=100)
elif view_type == "global" and is_admin_user:
memories = memory_manager.get_global_memories(limit=100)
else:
memories = memory_manager.get_user_memories(user_id=chat_id, limit=100)
view_type = "user"
total_cnt = len(memories)
page_size = 5
max_pages = max(1, (total_cnt + page_size - 1) // page_size) if total_cnt > 0 else 1
page = max(0, min(page, max_pages - 1))
paged_memories = memories[page * page_size : (page + 1) * page_size]
stats = memory_manager.get_stats(user_id=chat_id, project_name=proj_name)
if is_fa:
if view_type == "project":
tab_name = f"📁 خاطرات اختصاصی پروژه (<code>{escape_html(proj_name)}</code>)"
tab_desc = "این خاطرات <b>فقط هنگام فعالیت در همین پروژه</b> به هوش مصنوعی تزریق می‌شوند."
elif view_type == "user":
tab_name = "👤 خاطرات شخصی من (سراسری)"
tab_desc = "این خاطرات مربوط به شخص شماست و در <b>تمام پروژه‌ها</b> اعمال می‌گردد."
else:
tab_name = "🌐 قوانین و خاطرات عمومی (کل سرور و کاربران)"
tab_desc = "این قوانین برای <b>همه کاربران</b> تزریق می‌شوند (ویرایش و حذف فقط توسط مدیر)."
stats_str = f"<code>{stats['current_project_memories']}</code> پروژه | <code>{stats['current_user_memories']}</code> کاربر"
if is_admin_user:
stats_str += f" | <code>{stats['global_memories']}</code> عمومی"
header = (
f"🧠 <b>کنترل‌پنل حافظه هوشمند (AI Memory Dashboard)</b>\n\n"
f"• 📂 <b>بخش فعال:</b> {tab_name}\n"
f"• {tab_desc}\n"
f"• 📊 <b>آمار:</b> {stats_str}\n\n"
)
if not memories:
body = (
f"️ <i>در این بخش هنوز هیچ خاطره‌ای ثبت نشده است.</i>\n\n"
f"💡 <b>یادگیری خودکار:</b> هوش مصنوعی در حین گفتگوها نکات مهم مربوط به این بخش را خودکار استخراج و ثبت می‌کند."
)
else:
body_lines = [f"<b>لیست خاطرات (صفحه {page + 1} از {max_pages}):</b>\n"]
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
cat_badge = f"[{m.category}]" if m.category and m.category != "general" else ""
body_lines.append(
f"<b>{idx}.</b> 🔑 <code>{escape_html(m.key)}</code> {cat_badge}\n"
f" 📝 {escape_html(m.content)}"
)
body = "\n\n".join(body_lines)
else:
if view_type == "project":
tab_name = f"📁 Project Memory (<code>{escape_html(proj_name)}</code>)"
tab_desc = "Injected only when working in this project."
elif view_type == "user":
tab_name = "👤 User Profile Memory"
tab_desc = "Applies to you across all your projects."
else:
tab_name = "🌐 Global System Memory"
tab_desc = "Applies to all users (admin-managed)."
stats_str = f"<code>{stats['current_project_memories']}</code> project | <code>{stats['current_user_memories']}</code> user"
if is_admin_user:
stats_str += f" | <code>{stats['global_memories']}</code> global"
header = (
f"🧠 <b>AI Memory Dashboard</b>\n\n"
f"• 📂 <b>Active Tab:</b> {tab_name}\n"
f"• {tab_desc}\n"
f"• 📊 <b>Stats:</b> {stats_str}\n\n"
)
if not memories:
body = "️ <i>No memories recorded in this section yet.</i>"
else:
body_lines = [f"<b>Memories (Page {page + 1} of {max_pages}):</b>\n"]
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
cat_badge = f"[{m.category}]" if m.category and m.category != "general" else ""
body_lines.append(
f"<b>{idx}.</b> 🔑 <code>{escape_html(m.key)}</code> {cat_badge}\n"
f" 📝 {escape_html(m.content)}"
)
body = "\n\n".join(body_lines)
text = f"{header}{body}"
# Build keyboard
keyboard = []
# 1. Tab buttons (Global tab only visible to admin)
t_proj = f"📁 پروژه ({proj_name[:10]}) ✓" if view_type == "project" else f"📁 پروژه ({proj_name[:10]})"
t_user = "👤 کاربر (من) ✓" if view_type == "user" else "👤 کاربر (من)"
t_glob = "🌐 عمومی ✓" if view_type == "global" else "🌐 عمومی"
if not is_fa:
t_proj = f"📁 Project ✓" if view_type == "project" else f"📁 Project"
t_user = "👤 User ✓" if view_type == "user" else "👤 User"
t_glob = "🌐 Global ✓" if view_type == "global" else "🌐 Global"
tab_row = [
InlineKeyboardButton(t_proj, callback_data="mem_tab:project:0"),
InlineKeyboardButton(t_user, callback_data="mem_tab:user:0"),
]
if is_admin_user:
tab_row.append(InlineKeyboardButton(t_glob, callback_data="mem_tab:global:0"))
keyboard.append(tab_row)
# 2. Item delete buttons
can_delete_current = (view_type in ("project", "user")) or (view_type == "global" and is_admin_user)
if can_delete_current and paged_memories:
del_row = []
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
del_row.append(InlineKeyboardButton(f"🗑️ #{idx}", callback_data=f"mem_del_conf:{m.id}:{view_type}:{page}"))
if len(del_row) == 5:
keyboard.append(del_row)
del_row = []
if del_row:
keyboard.append(del_row)
# 3. Pagination row
if max_pages > 1:
pag_row = []
if page > 0:
pag_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"mem_tab:{view_type}:{page - 1}"))
pag_row.append(InlineKeyboardButton(f"📄 {page + 1}/{max_pages}", callback_data="noop"))
if page < max_pages - 1:
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"mem_tab:{view_type}:{page + 1}"))
keyboard.append(pag_row)
# 4. Action buttons (Add, Clear, Refresh)
action_row = [
InlineKeyboardButton("➕ افزودن خاطره" if is_fa else " Add Memory", callback_data=f"mem_add_menu:{view_type}"),
]
if can_delete_current and total_cnt > 0:
clear_label = "🧹 پاکسازی این بخش" if is_fa else "🧹 Clear this section"
action_row.append(InlineKeyboardButton(clear_label, callback_data=f"mem_clear_conf:{view_type}"))
action_row.append(InlineKeyboardButton("🔄 تازه‌سازی" if is_fa else "🔄 Refresh", callback_data=f"mem_tab:{view_type}:{page}"))
keyboard.append(action_row)
# 5. Main menu button
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("⚙️ تنظیمات" if is_fa else "⚙️ Settings", callback_data="btn_settings_menu"),
])
return text, InlineKeyboardMarkup(keyboard)
MEMORY_PRESETS = {
"project": {
"php_caddy": {
"key": "php_caddy_stack",
"category": "tech_stack",
"content": "همیشه از PHP 8.x مدرن به عنوان زبان اصلی بک‌اند و سرور Caddy برای Reverse Proxy و مدیریت خودکار SSL دامنه msa.artacloud.ir استفاده شود.",
"content_en": "Always use modern PHP 8.x as backend and Caddy reverse proxy for msa.artacloud.ir domains with automatic SSL.",
},
"qa_tests": {
"key": "qa_and_verification",
"category": "rule",
"content": "قبل از اتمام کار، تمام کدها، مسیرها، سینتکس و سرویس‌ها باید به طور کامل تست، عیب‌یابی و اعتبارسنجی شوند.",
"content_en": "Always test, verify syntax, and perform QA audit before marking tasks complete.",
},
"clean_code": {
"key": "clean_code_standards",
"category": "rule",
"content": "ساختار تمیز دایرکتوری‌ها، تفکیک فایل‌ها، کامنت‌های معنادار و الگوهای خوانا رعایت شوند.",
"content_en": "Follow clean code architecture, proper folder structure, and meaningful documentation.",
},
},
"user": {
"lang_fa": {
"key": "preferred_language",
"category": "preference",
"content": "همیشه تمام مکالمات، توضیحات، و پیام‌ها به زبان فارسی روان و دقیق ارائه شوند.",
"content_en": "Always communicate and explain in fluent Persian.",
},
"concise_style": {
"key": "response_style",
"category": "preference",
"content": "پاسخ‌ها خلاصه، سریع، دقیق، ساختاریافته و با بولت‌پوینت‌های خوانا ارسال شوند.",
"content_en": "Responses should be concise, well-structured, and easy to read.",
},
"tz_tehran": {
"key": "preferred_timezone",
"category": "preference",
"content": "منطقه زمانی کاربر Asia/Tehran (ایران) است و زمان‌بندی‌ها بر اساس این منطقه انجام شوند.",
"content_en": "User timezone is Asia/Tehran.",
},
"php_pref": {
"key": "preferred_tech_stack",
"category": "preference",
"content": "ترجیح زبان برنامه‌نویسی و پشته توسعه دهنده PHP مدرن (PHP 8.x) است.",
"content_en": "Developer prefers modern PHP 8.x tech stack.",
},
},
"global": {
"user_isolation": {
"key": "user_isolation_security",
"category": "rule",
"content": "کاربران عادی به جز مدیر فقط حق کار درون دایرکتوری اختصاصی پروژه خود (/root/projects/{user_id}/...) را دارند و هرگز نباید پرامپت‌ها یا داده‌های سیستمی به آن‌ها افشا شود.",
"content_en": "Non-admin users are strictly isolated to their own project directories.",
},
"caddy_ssl": {
"key": "server_domain_caddy",
"category": "rule",
"content": "تمام سرویس‌های وب و سابدامین‌های *.msa.artacloud.ir باید با Caddy با SSL خودکار تنظیم و فعال شوند.",
"content_en": "All web services and subdomains are routed through Caddy with automatic SSL.",
},
"hierarchy_rule": {
"key": "precedence_hierarchy",
"category": "rule",
"content": "سلسلهمراتب قطعی اولویت: ۱. قوانین عمومی سیستم ۲. خاطرات و تصمیمات اختصاصی پروژه ۳. ترجیحات کلی کاربر.",
"content_en": "Strict precedence: 1. Global rules, 2. Project memory, 3. User preferences.",
},
},
}
def build_memory_add_menu(chat_id: int, view_type: str = "project") -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive memory creation / template guide menu with quick presets and copyable command."""
from memory_manager import memory_manager
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
proj_name = curr_proj.name if curr_proj else "default"
if view_type == "global" and not is_admin_user:
view_type = "project" if curr_proj else "user"
if view_type not in ("project", "user", "global"):
view_type = "project" if curr_proj else "user"
if is_fa:
type_labels = {
"project": f"📁 پروژه اختصاصی (<code>{escape_html(proj_name)}</code>)",
"user": "👤 ترجیحات و مشخصات شخصی من (سراسری)",
"global": "🌐 قوانین عمومی سیستم (کل سرور)",
}
text = (
f"🧠 <b>افزودن خاطره به حافظه هوش مصنوعی (Add Memory)</b>\n\n"
f"• 🎯 <b>بخش انتخابی:</b> {type_labels.get(view_type, view_type)}\n\n"
f"💡 <b>روش‌های ثبت خاطره:</b>\n\n"
f"📌 <b>۱. ثبت سریع با الگوهای آماده (با یک کلیک):</b>\n"
f"با لمس هر یک از دکمه‌های زیر، خاطره استاندارد بلافاصله در این بخش ذخیره می‌شود.\n\n"
f"📌 <b>۲. ثبت دستی با دستور مستقیم (کپی و ویرایش):</b>\n"
f"<code>/memory add {view_type} &lt;کلید&gt; | &lt;متن خاطره یا قانون&gt;</code>\n\n"
f"<b>مثال:</b>\n"
f"• <code>/memory add {view_type} code_style | کدهای پایتون و PHP به صورت ماژولار و تمیز نوشته شوند</code>\n\n"
f"📌 <b>۳. آموزش مستقیم در چت با هوش مصنوعی:</b>\n"
f"در حین گفتگو بنویسید: <i>«این نکته رو در حافظه ثبت کن: پورت دیتابیس ۵۴۳۲ است»</i> تا خودکار به خاطر بسپارد."
)
else:
type_labels = {
"project": f"📁 Active Project (<code>{escape_html(proj_name)}</code>)",
"user": "👤 Personal Profile / Preferences",
"global": "🌐 Global System Rules",
}
text = (
f"🧠 <b>Add AI Long-Term Memory</b>\n\n"
f"• 🎯 <b>Target Tier:</b> {type_labels.get(view_type, view_type)}\n\n"
f"💡 <b>Methods to Add Memory:</b>\n\n"
f"📌 <b>1. One-Click Quick Presets:</b>\n"
f"Click any button below to instantly save standard rules or preferences.\n\n"
f"📌 <b>2. Manual Command:</b>\n"
f"<code>/memory add {view_type} &lt;key&gt; | &lt;content&gt;</code>\n\n"
f"<b>Example:</b>\n"
f"• <code>/memory add {view_type} code_style | Write clean modular code</code>\n\n"
f"📌 <b>3. Direct Chat with AI:</b>\n"
f"Simply tell AI in chat: <i>'Remember that PostgreSQL port is 5432'</i>."
)
keyboard = []
# 1. Tier selection tabs
t_proj = f"📁 پروژه ({proj_name[:10]}) ✓" if view_type == "project" else f"📁 پروژه ({proj_name[:10]})"
t_user = "👤 کاربر (من) ✓" if view_type == "user" else "👤 کاربر (من)"
t_glob = "🌐 عمومی ✓" if view_type == "global" else "🌐 عمومی"
if not is_fa:
t_proj = "📁 Project ✓" if view_type == "project" else "📁 Project"
t_user = "👤 User ✓" if view_type == "user" else "👤 User"
t_glob = "🌐 Global ✓" if view_type == "global" else "🌐 Global"
tab_row = [
InlineKeyboardButton(t_proj, callback_data="mem_add_tab:project"),
InlineKeyboardButton(t_user, callback_data="mem_add_tab:user"),
]
if is_admin_user:
tab_row.append(InlineKeyboardButton(t_glob, callback_data="mem_add_tab:global"))
keyboard.append(tab_row)
# 2. Preset buttons based on view_type
if view_type == "project":
if is_fa:
keyboard.append([
InlineKeyboardButton("🐘 اولویت با PHP مدرن و Caddy", callback_data="mem_add_preset:project:php_caddy"),
])
keyboard.append([
InlineKeyboardButton("🛡️ اجرای تست و اعتبارسنجی کامل", callback_data="mem_add_preset:project:qa_tests"),
])
keyboard.append([
InlineKeyboardButton("📁 تمیزی و سازماندهی کدها", callback_data="mem_add_preset:project:clean_code"),
])
else:
keyboard.append([
InlineKeyboardButton("🐘 Prefer Modern PHP & Caddy", callback_data="mem_add_preset:project:php_caddy"),
])
keyboard.append([
InlineKeyboardButton("🛡️ Strict QA Testing & Audit", callback_data="mem_add_preset:project:qa_tests"),
])
keyboard.append([
InlineKeyboardButton("📁 Clean Code & Standards", callback_data="mem_add_preset:project:clean_code"),
])
elif view_type == "user":
if is_fa:
keyboard.append([
InlineKeyboardButton("🇮🇷 پاسخ‌ها همیشه فارسی روان باشد", callback_data="mem_add_preset:user:lang_fa"),
])
keyboard.append([
InlineKeyboardButton("📝 پاسخ‌های فشرده و ساختاریافته", callback_data="mem_add_preset:user:concise_style"),
])
keyboard.append([
InlineKeyboardButton("⏰ منطقه زمانی Asia/Tehran", callback_data="mem_add_preset:user:tz_tehran"),
])
keyboard.append([
InlineKeyboardButton("🐘 ترجیح اولویت توسعه با PHP", callback_data="mem_add_preset:user:php_pref"),
])
else:
keyboard.append([
InlineKeyboardButton("🇮🇷 Always Respond in Persian", callback_data="mem_add_preset:user:lang_fa"),
])
keyboard.append([
InlineKeyboardButton("📝 Concise & Structured Style", callback_data="mem_add_preset:user:concise_style"),
])
keyboard.append([
InlineKeyboardButton("⏰ Timezone Asia/Tehran", callback_data="mem_add_preset:user:tz_tehran"),
])
keyboard.append([
InlineKeyboardButton("🐘 Prefer PHP Stack", callback_data="mem_add_preset:user:php_pref"),
])
elif view_type == "global" and is_admin_user:
if is_fa:
keyboard.append([
InlineKeyboardButton("🔒 قرنطینه و تفکیک امنیتی کاربران", callback_data="mem_add_preset:global:user_isolation"),
])
keyboard.append([
InlineKeyboardButton("🌐 رورس‌پروکسی Caddy و SSL خودکار", callback_data="mem_add_preset:global:caddy_ssl"),
])
keyboard.append([
InlineKeyboardButton("⚖️ رعایت سلسلهمراتب ۳ سطحی حافظه", callback_data="mem_add_preset:global:hierarchy_rule"),
])
else:
keyboard.append([
InlineKeyboardButton("🔒 User Isolation & Security", callback_data="mem_add_preset:global:user_isolation"),
])
keyboard.append([
InlineKeyboardButton("🌐 Caddy SSL Reverse Proxy", callback_data="mem_add_preset:global:caddy_ssl"),
])
keyboard.append([
InlineKeyboardButton("⚖️ Strict Memory Precedence", callback_data="mem_add_preset:global:hierarchy_rule"),
])
# 3. Navigation row
keyboard.append([
InlineKeyboardButton("🔙 لیست خاطرات" if is_fa else "🔙 Memory List", callback_data=f"mem_tab:{view_type}:0"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(keyboard)
def build_tasks_menu(chat_id: int, page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive Scheduled Tasks manager dashboard with pagination and status buttons."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
curr_proj = session_manager.get_current_project(chat_id)
# Privacy: Strictly retrieve tasks owned by or created by this chat_id
all_user_tasks = task_scheduler.get_user_tasks(chat_id)
# Sort: active first (by next_run), then paused, then completed/failed (by last_run desc)
def sort_key(t: ScheduledTask):
status_rank = {"active": 0, "paused": 1, "completed": 2, "failed": 3}.get(t.status, 4)
ts = t.next_run_timestamp if t.status == "active" else -(t.last_run_timestamp or 0)
return (status_rank, ts)
all_user_tasks.sort(key=sort_key)
active_cnt = sum(1 for t in all_user_tasks if t.status == "active")
paused_cnt = sum(1 for t in all_user_tasks if t.status == "paused")
completed_cnt = sum(1 for t in all_user_tasks if t.status in ("completed", "failed"))
total_cnt = len(all_user_tasks)
page_size = 5
max_pages = max(1, (total_cnt + page_size - 1) // page_size) if total_cnt > 0 else 1
page = max(0, min(page, max_pages - 1))
start_idx = page * page_size
page_tasks = all_user_tasks[start_idx : start_idx + page_size]
if is_fa:
header = (
f"⏰ <b>مدیریت تسک‌های زمان‌بندی شده (Scheduled Tasks)</b>\n\n"
f"• 📊 <b>وضعیت تسک‌ها:</b> <code>{total_cnt} تسک</code> (🟢 {active_cnt} فعال | ⏸️ {paused_cnt} متوقف | ✅ {completed_cnt} پایان‌یافته)\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'نامشخص')}</code>\n\n"
)
else:
header = (
f"⏰ <b>Scheduled Tasks Manager</b>\n\n"
f"• 📊 <b>Task Stats:</b> <code>{total_cnt}</code> (🟢 {active_cnt} Active | ⏸️ {paused_cnt} Paused | ✅ {completed_cnt} Completed)\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name if curr_proj else 'None')}</code>\n\n"
)
body_lines = []
keyboard = []
if not all_user_tasks:
if is_fa:
body_lines.append("<i>هنوز هیچ تسک زمان‌بندی شده‌ای ثبت نکرده‌اید.</i>\n")
else:
body_lines.append("<i>No scheduled tasks registered yet.</i>\n")
else:
for idx, t in enumerate(page_tasks, start=start_idx + 1):
status_icon = "🟢" if t.status == "active" else "⏸️" if t.status == "paused" else "✅" if t.status == "completed" else "❌"
type_icon = "🧠" if t.task_type == "prompt" else "⚡" if t.task_type == "command" else "🔔"
title_clean = (t.title or t.content).replace("\n", " ").strip()
if len(title_clean) > 35:
title_clean = title_clean[:32] + "..."
next_str = ""
if t.status == "active":
diff = t.next_run_timestamp - time.time()
next_str = f"⏳ {format_relative_time(diff, is_fa=is_fa)}"
elif t.status == "paused":
next_str = "⏸️ متوقف" if is_fa else "⏸️ Paused"
else:
next_str = "✅ انجام شد" if is_fa else "✅ Done"
runs_badge = f" [تکرار: {t.total_runs}/{t.max_runs}]" if t.max_runs else ""
if is_fa:
body_lines.append(
f"{status_icon} <b>#{idx}</b> <code>{t.id}</code> ({type_icon} {escape_html(t.timing_spec)}{runs_badge})\n"
f" └ <b>پروژه:</b> <code>{escape_html(t.project_name)}</code> | {next_str}\n"
f" └ <i>«{escape_html(title_clean)}»</i>\n"
)
else:
body_lines.append(
f"{status_icon} <b>#{idx}</b> <code>{t.id}</code> ({type_icon} {escape_html(t.timing_spec)}{runs_badge})\n"
f" └ <b>Proj:</b> <code>{escape_html(t.project_name)}</code> | {next_str}\n"
f" └ <i>\"{escape_html(title_clean)}\"</i>\n"
)
# Button for this task
btn_label = f"{status_icon} #{idx} [{t.id}] {t.timing_spec}"
keyboard.append([
InlineKeyboardButton(btn_label[:38], callback_data=f"task_detail:{t.id}"),
InlineKeyboardButton("▶️", callback_data=f"task_run:{t.id}"),
InlineKeyboardButton("🗑️", callback_data=f"task_del:{t.id}"),
])
# Pagination row
if max_pages > 1:
pag_row = []
if page > 0:
pag_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"tasks_page:{page-1}"))
pag_row.append(InlineKeyboardButton(f"📄 {page+1}/{max_pages}", callback_data=f"tasks_page:{page}"))
if page < max_pages - 1:
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"tasks_page:{page+1}"))
keyboard.append(pag_row)
# Actions row
action_row = [
InlineKeyboardButton("➕ افزودن تسک جدید" if is_fa else " Add New Task", callback_data="task_new_guide"),
InlineKeyboardButton("🔄 بروزرسانی" if is_fa else "🔄 Refresh", callback_data=f"tasks_page:{page}"),
]
keyboard.append(action_row)
if completed_cnt > 0:
keyboard.append([
InlineKeyboardButton("🧹 پاکسازی پایان‌یافته‌ها" if is_fa else "🧹 Clear Completed", callback_data="task_clear_completed"),
])
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
])
if is_fa:
footer = (
f"\n💡 <i>دستور ایجاد سریع:</i>\n"
f"<code>/schedule &lt;زمان‌بندی&gt; | &lt;پرامپت یا دستور&gt;</code>\n"
f"<b>مثال:</b> <code>/schedule every 1h (3 بار) | بررسی سلامت سرور</code>"
)
else:
footer = (
f"\n💡 <i>Quick create command:</i>\n"
f"<code>/schedule &lt;timing&gt; | &lt;prompt or cmd&gt;</code>\n"
f"<b>Example:</b> <code>/schedule every 1h (3 times) | check server health</code>"
)
return header + "".join(body_lines) + footer, InlineKeyboardMarkup(keyboard)
def build_task_detail_menu(chat_id: int, task_id: str) -> tuple[str, InlineKeyboardMarkup]:
"""Builds detailed view for a single scheduled task with control buttons (Strict Privacy Enforced)."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
task = task_scheduler.get_task(task_id)
# Strict Privacy: Check that task belongs to the requesting user
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
text = "❌ <b>تسک مورد نظر یافت نشد یا دسترسی به آن مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>"
keyboard = [[InlineKeyboardButton("⏰ بازگشت به لیست تسک‌ها" if is_fa else "⏰ Back to Tasks", callback_data="btn_tasks_menu")]]
return text, InlineKeyboardMarkup(keyboard)
status_str = {
"active": "🟢 فعال (Active)",
"paused": "⏸️ متوقف شده (Paused)",
"completed": "✅ پایان‌یافته (Completed)",
"failed": "❌ خطا در اجرا (Failed)",
}.get(task.status, task.status)
type_str = {
"prompt": "🧠 پرامپت هوش مصنوعی (AI Prompt)",
"command": "⚡ دستور شل لینوکس (Shell Command)",
"reminder": "🔔 یادآور پیام (Reminder)",
}.get(task.task_type, task.task_type)
next_rel = format_relative_time(task.next_run_timestamp - time.time(), is_fa=is_fa) if (task.status == "active") else ("—" if is_fa else "N/A")
next_exact = format_timestamp(task.next_run_timestamp, is_fa=is_fa) if (task.status == "active") else ("—" if is_fa else "N/A")
last_run_str = format_timestamp(task.last_run_timestamp, is_fa=is_fa) if task.last_run_timestamp else ("هنوز اجرا نشده" if is_fa else "Never")
repeat_label = f"{task.max_runs} بار" if task.max_runs else ("نامحدود (دائمی)" if is_fa else "Unlimited")
if is_fa:
text = (
f"🔍 <b>جزئیات تسک زمان‌بندی شده (Task Details)</b>\n\n"
f"• 🏷️ <b>شناسه تسک:</b> <code>{task.id}</code>\n"
f"• 📊 <b>وضعیت:</b> {status_str}\n"
f"• 🛠 <b>نوع تسک:</b> {type_str}\n"
f"• 📁 <b>پروژه هدف:</b> <code>{escape_html(task.project_name)}</code>\n"
f"• ⏱️ <b>فرمول زمان‌بندی:</b> <code>{escape_html(task.timing_spec)}</code>\n"
f"• 🔄 <b>سقف تکرار (Max Runs):</b> <code>{repeat_label}</code>\n"
f"• 🔢 <b>تعداد دفعات اجرا شده:</b> <code>{task.total_runs} بار</code>\n"
f"• ⏳ <b>اجرای بعدی:</b> {next_rel} (<code>{next_exact}</code>)\n"
f"• 🕒 <b>آخرین اجرا:</b> <code>{last_run_str}</code> (وضعیت: {task.last_run_status or 'ندارد'})\n\n"
f"📝 <b>محتوا و دستور تسک:</b>\n"
f"<pre>{escape_html(task.content)}</pre>\n"
)
if task.last_run_result:
snippet = task.last_run_result[:250] + ("..." if len(task.last_run_result) > 250 else "")
text += f"\n📋 <b>پیش‌نمایش آخرین خروجی:</b>\n<i>«{escape_html(snippet)}»</i>\n"
else:
text = (
f"🔍 <b>Scheduled Task Details</b>\n\n"
f"• 🏷️ <b>Task ID:</b> <code>{task.id}</code>\n"
f"• 📊 <b>Status:</b> {status_str}\n"
f"• 🛠 <b>Type:</b> {type_str}\n"
f"• 📁 <b>Target Project:</b> <code>{escape_html(task.project_name)}</code>\n"
f"• ⏱️ <b>Timing Spec:</b> <code>{escape_html(task.timing_spec)}</code>\n"
f"• 🔄 <b>Max Runs:</b> <code>{repeat_label}</code>\n"
f"• 🔢 <b>Total Runs:</b> <code>{task.total_runs}</code>\n"
f"• ⏳ <b>Next Run:</b> {next_rel} (<code>{next_exact}</code>)\n"
f"• 🕒 <b>Last Run:</b> <code>{last_run_str}</code> (Status: {task.last_run_status or 'N/A'})\n\n"
f"📝 <b>Content / Command:</b>\n"
f"<pre>{escape_html(task.content)}</pre>\n"
)
if task.last_run_result:
snippet = task.last_run_result[:250] + ("..." if len(task.last_run_result) > 250 else "")
text += f"\n📋 <b>Last Output Preview:</b>\n<i>\"{escape_html(snippet)}\"</i>\n"
keyboard = [
[
InlineKeyboardButton("▶️ اجرای فوری" if is_fa else "▶️ Run Now", callback_data=f"task_run:{task.id}"),
InlineKeyboardButton(
"⏸️ متوقف کردن" if task.status == "active" else "▶️ فعال‌سازی مجدد",
callback_data=f"task_pause:{task.id}" if task.status == "active" else f"task_resume:{task.id}"
)
],
]
if task.last_run_result:
keyboard.append([
InlineKeyboardButton("📜 مشاهده کامل آخرین خروجی" if is_fa else "📜 View Full Last Output", callback_data=f"task_last_out:{task.id}"),
InlineKeyboardButton("🗑️ حذف این تسک" if is_fa else "🗑️ Delete Task", callback_data=f"task_del:{task.id}"),
])
else:
keyboard.append([
InlineKeyboardButton("🗑️ حذف این تسک" if is_fa else "🗑️ Delete Task", callback_data=f"task_del:{task.id}"),
])
keyboard.append([
InlineKeyboardButton("⏰ لیست تسک‌ها" if is_fa else "⏰ Back to Tasks", callback_data="btn_tasks_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(keyboard)
def build_task_add_guide(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive guide and templates for scheduling tasks."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if is_fa:
text = (
f"⏰ <b>راهنمای زمان‌بندی تسک‌ها (Scheduled Tasks Guide)</b>\n\n"
f"شما می‌توانید هرگونه دستور، پرامپت هوش مصنوعی، یا اسکریپت را با زمان‌بندی دقیق و تعداد تکرار دلخواه در ربات تنظیم کنید تا سر وقت اجرا شده و نتیجه برای شما در تلگرام ارسال شود.\n\n"
f"💡 <b>فرمت دستور سریع:</b>\n"
f"<code>/schedule &lt;زمان‌بندی [تعداد تکرار]&gt; | &lt;متن دستور یا پرامپت&gt;</code>\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"📌 <b>۱. اجرای تکرارشونده و دوره‌ای با سقف تکرار:</b>\n"
f"• <code>/schedule every 1h (5 بار) | بررسی سلامت سرور و فضای دیسک</code>\n"
f"• <code>/schedule هر ۳۰ دقیقه (۳ بار) | لاگ‌های خطا را بررسی و خلاصه کن</code>\n"
f"• <code>/schedule every 24h | گزارش وضعیت پیشرفت پروژه</code> (نامحدود)\n"
f"• <code>/schedule 0 23 * * * | خلاصه وظایف امروز را آماده کن</code> (هر شب ساعت ۱۱)\n\n"
f"📌 <b>۲. ثبت طبیعی از طریق چت مستقیم با هوش مصنوعی:</b>\n"
f"• کافیست در چت بنویسید: <i>«هر ۱۰ دقیقه ۳ بار به من یادآوری کن پاشم»</i> یا <i>«هر شب ساعت ۲۳ لاگ‌ها رو تحلیل کن»</i> و هوش مصنوعی تسک را ثبت می‌کند.\n\n"
f"📌 <b>۳. اجرای یک‌باره در آینده (Relative / Exact Time):</b>\n"
f"• <code>/schedule in 15m | گزارش وضعیت تست‌ها را بده</code>\n"
f"• <code>/schedule ۱۰ دقیقه بعد | وضعیت پروژه را چک کن</code>\n"
f"• <code>/schedule 14:30 | خلاصه فعالیت‌های امروز را ارسال کن</code>\n"
f"• <code>/schedule فردا 09:00 | ریپازیتوری را بررسی کن</code>\n\n"
f"📌 <b>۴. اجرای دستورات شل لینوکس (پیشوند cmd:):</b>\n"
f"• <code>/schedule every 2h (3 بار) | cmd: git pull &amp;&amp; npm test</code>\n\n"
f"📌 <b>۵. یادآوری و نوتیفیکیشن ساده (پیشوند remind:):</b>\n"
f"• <code>/schedule in 45m | remind: تماس با مدیر پروژه</code>\n\n"
f"🔒 <i>تمام تسک‌های شما خصوصی بوده و فقط توسط خود شما قابل مشاهده و مدیریت است.</i>"
)
keyboard = [
[
InlineKeyboardButton("⏰ لیست تسک‌های من", callback_data="btn_tasks_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 بستن", callback_data="proj_close"),
]
]
else:
text = (
f"⏰ <b>Scheduled Tasks Guide</b>\n\n"
f"You can schedule any AI prompt, shell command, or reminder with recurrence and max repetitions.\n\n"
f"💡 <b>Command Format:</b>\n"
f"<code>/schedule &lt;timing [repeats]&gt; | &lt;prompt or command&gt;</code>\n\n"
f"━━━━━━━━━━━━━━━━━━━━\n"
f"📌 <b>1. Recurring Tasks with Max Runs:</b>\n"
f"• <code>/schedule every 1h (3 times) | check server health</code>\n"
f"• <code>/schedule 0 23 * * * | daily code summary</code> (Every night at 11 PM)\n\n"
f"📌 <b>2. AI Natural Scheduling:</b>\n"
f"• Just ask the AI in chat: <i>\"remind me every 10m 5 times to take a break\"</i>\n\n"
f"🔒 <i>All tasks are strictly private to your user account.</i>"
)
keyboard = [
[
InlineKeyboardButton("⏰ My Tasks", callback_data="btn_tasks_menu"),
InlineKeyboardButton("🏠 Dashboard", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
]
]
return text, InlineKeyboardMarkup(keyboard)
def build_users_menu(chat_id: int, page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive User Whitelist & Invitation Manager for Admins."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
allowed_users = list(settings.allowed_user_ids)
admin_users = list(settings.admin_user_ids)
all_known_uids = list(dict.fromkeys(admin_users + allowed_users))
pending_reqs = invite_manager.get_pending_requests()
active_invs = invite_manager.get_active_invites()
page_size = 6
total_cnt = len(all_known_uids)
max_pages = max(1, (total_cnt + page_size - 1) // page_size) if total_cnt > 0 else 1
page = max(0, min(page, max_pages - 1))
start_idx = page * page_size
page_uids = all_known_uids[start_idx : start_idx + page_size]
if is_fa:
header = (
f"👥 <b>پنل مدیریت کاربران و دعوت‌ها (Admin User Manager)</b>\n\n"
f"• 👥 <b>تعداد کل کاربران مجاز:</b> <code>{total_cnt}</code> (👑 {len(admin_users)} مدیر | 👤 {len(allowed_users)} کاربر عادی)\n"
f"• 📩 <b>درخواست‌های دسترسی در انتظار:</b> <code>{len(pending_reqs)}</code>\n"
f"• 🔗 <b>لینک‌های دعوت فعال:</b> <code>{len(active_invs)}</code>\n\n"
f"📋 <b>لیست کاربران فعال سیستم:</b>\n"
)
else:
header = (
f"👥 <b>Admin User & Invite Management</b>\n\n"
f"• 👥 <b>Total Authorized Users:</b> <code>{total_cnt}</code> (👑 {len(admin_users)} Admins | 👤 {len(allowed_users)} Users)\n"
f"• 📩 <b>Pending Access Requests:</b> <code>{len(pending_reqs)}</code>\n"
f"• 🔗 <b>Active Invite Links:</b> <code>{len(active_invs)}</code>\n\n"
f"📋 <b>Authorized Users List:</b>\n"
)
body_lines = []
keyboard = []
for idx, uid in enumerate(page_uids, start=start_idx + 1):
is_admin = uid in admin_users
role_tag = "👑 ادمین" if is_admin else "👤 کاربر"
role_tag_en = "👑 Admin" if is_admin else "👤 User"
body_lines.append(
f"• <b>#{idx}</b> <code>{uid}</code> ({role_tag if is_fa else role_tag_en})\n"
)
row = [InlineKeyboardButton(f"👤 {uid} ({'Admin' if is_admin else 'User'})", callback_data=f"user_info:{uid}")]
if not is_admin:
row.append(InlineKeyboardButton("🚫 لغو دسترسی", callback_data=f"user_revoke_ask:{uid}"))
keyboard.append(row)
# Pagination
if max_pages > 1:
pag_row = []
if page > 0:
pag_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"users_page:{page-1}"))
pag_row.append(InlineKeyboardButton(f"📄 {page+1}/{max_pages}", callback_data=f"users_page:{page}"))
if page < max_pages - 1:
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"users_page:{page+1}"))
keyboard.append(pag_row)
# Quick action rows
keyboard.append([
InlineKeyboardButton("🔗 ساخت لینک دعوت جدید" if is_fa else "🔗 Generate Invite Link", callback_data="btn_gen_invite"),
])
sub_actions = []
if active_invs:
sub_actions.append(InlineKeyboardButton(f"📋 لینک‌های فعال ({len(active_invs)})" if is_fa else f"📋 Active Links ({len(active_invs)})", callback_data="btn_invites_menu"))
if pending_reqs:
sub_actions.append(InlineKeyboardButton(f"📩 درخواست‌ها ({len(pending_reqs)})" if is_fa else f"📩 Requests ({len(pending_reqs)})", callback_data="btn_pending_reqs"))
if sub_actions:
keyboard.append(sub_actions)
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
])
if is_fa:
footer = (
f"\n💡 <i>دستورات متنی ادمین:</i>\n"
f"• دعوت با شناسه: <code>/invite &lt;user_id&gt;</code>\n"
f"• لغو دسترسی کاربر: <code>/uninvite &lt;user_id&gt;</code>\n"
f"• ساخت لینک دعوت: <code>/invitelink</code>"
)
else:
footer = (
f"\n💡 <i>Admin text commands:</i>\n"
f"• Invite by ID: <code>/invite &lt;user_id&gt;</code>\n"
f"• Revoke user: <code>/uninvite &lt;user_id&gt;</code>\n"
f"• Create invite link: <code>/invitelink</code>"
)
return header + "".join(body_lines) + footer, InlineKeyboardMarkup(keyboard)
def build_invites_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Builds active invite links browser for Admins."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
active_invs = invite_manager.get_active_invites()
if is_fa:
header = f"🔗 <b>لینک‌های دعوت فعال سیستم ({len(active_invs)} لینک)</b>\n\n"
else:
header = f"🔗 <b>Active Invite Links ({len(active_invs)} links)</b>\n\n"
body_lines = []
keyboard = []
if not active_invs:
if is_fa:
body_lines.append("<i>در حال حاضر هیچ لینک دعوت فعالی وجود ندارد.</i>\n")
else:
body_lines.append("<i>No active invite links currently.</i>\n")
else:
for idx, inv in enumerate(active_invs, start=1):
cap_str = f"{inv.uses_count}/{inv.max_uses}" if inv.max_uses > 0 else f"{inv.uses_count}/نامحدود"
exp_str = format_timestamp(inv.expires_at, is_fa=is_fa) if inv.expires_at else ("دائمی" if is_fa else "Never")
if is_fa:
body_lines.append(
f"• <b>#{idx}</b> <code>{inv.code}</code>\n"
f" └ <b>ظرفیت مصرف:</b> {cap_str} | <b>انقضا:</b> {exp_str}\n"
)
else:
body_lines.append(
f"• <b>#{idx}</b> <code>{inv.code}</code>\n"
f" └ <b>Usage:</b> {cap_str} | <b>Expires:</b> {exp_str}\n"
)
keyboard.append([
InlineKeyboardButton(f"🔗 {inv.code} ({cap_str})", callback_data=f"invite_info:{inv.code}"),
InlineKeyboardButton("🗑️ ابطال", callback_data=f"invite_revoke:{inv.code}"),
])
keyboard.append([
InlineKeyboardButton("➕ ساخت لینک دعوت جدید" if is_fa else " Create New Link", callback_data="btn_gen_invite"),
])
keyboard.append([
InlineKeyboardButton("👥 مدیریت کاربران" if is_fa else "👥 User Manager", callback_data="btn_users_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return header + "".join(body_lines), InlineKeyboardMarkup(keyboard)
def build_pending_requests_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Builds pending access requests browser for Admins."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
reqs = invite_manager.get_pending_requests()
if is_fa:
header = f"📩 <b>درخواست‌های دسترسی کاربران جدید ({len(reqs)} درخواست)</b>\n\n"
else:
header = f"📩 <b>Pending Access Requests ({len(reqs)} requests)</b>\n\n"
body_lines = []
keyboard = []
if not reqs:
if is_fa:
body_lines.append("<i>هیچ درخواست دسترسی جدیدی در انتظار نیست.</i>\n")
else:
body_lines.append("<i>No pending access requests.</i>\n")
else:
for idx, r in enumerate(reqs, start=1):
time_str = format_timestamp(r.requested_at, is_fa=is_fa)
username_tag = f"@{r.username}" if r.username else "(ندارد)"
if is_fa:
body_lines.append(
f"• <b>#{idx}</b> 👤 <b>{escape_html(r.full_name)}</b>\n"
f" └ 🆔 <code>{r.user_id}</code> | {username_tag} | 🕒 {time_str}\n"
)
else:
body_lines.append(
f"• <b>#{idx}</b> 👤 <b>{escape_html(r.full_name)}</b>\n"
f" └ 🆔 <code>{r.user_id}</code> | {username_tag} | 🕒 {time_str}\n"
)
keyboard.append([
InlineKeyboardButton(f"✅ تایید #{idx} ({r.user_id})", callback_data=f"req_approve:{r.user_id}"),
InlineKeyboardButton(f"❌ رد #{idx}", callback_data=f"req_deny:{r.user_id}"),
])
keyboard.append([
InlineKeyboardButton("👥 مدیریت کاربران" if is_fa else "👥 User Manager", callback_data="btn_users_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
return header + "".join(body_lines), InlineKeyboardMarkup(keyboard)
def build_server_hardware_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Builds interactive Server Hardware (RAM/CPU/Disk) monitor view."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
text = render_server_hardware_report(is_fa=is_fa)
keyboard = [
[
InlineKeyboardButton("🔄 بروزرسانی لحظه‌ای" if is_fa else "🔄 Refresh Stats", callback_data="btn_hw_refresh"),
InlineKeyboardButton("📈 سهمیه مصرف AGY" if is_fa else "📈 AGY Quota", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
]
]
return text, InlineKeyboardMarkup(keyboard)
# Command: /start or /menu or /dashboard
@check_auth
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
user_id = update.effective_user.id
user_name = update.effective_user.first_name or "User"
username = update.effective_user.username or ""
# Check for invite code in start parameter: /start inv_xxxx
if context.args and context.args[0].startswith("inv_"):
code = context.args[0].strip()
success, msg, token = invite_manager.validate_and_use_invite(code, user_id)
if success:
logger.info(f"User {user_id} joined via invite token {code}")
# Notify admins
for admin_id in settings.admin_user_ids:
if admin_id != user_id:
try:
admin_notif = (
f"🎉 <b>کاربر جدید با لینک دعوت وارد شد!</b>\n\n"
f"• 👤 <b>نام:</b> {escape_html(user_name)}\n"
f"• 🆔 <b>شناسه عددی (User ID):</b> <code>{user_id}</code>\n"
f"• 🏷️ <b>یوزرنیم:</b> @{username}\n"
f"• 🔗 <b>کد دعوت:</b> <code>{code}</code>"
)
await context.application.bot.send_message(
chat_id=admin_id,
text=admin_notif,
parse_mode=constants.ParseMode.HTML,
)
except Exception as err:
logger.error(f"Failed to notify admin {admin_id}: {err}")
welcome_msg = (
f"🎉 <b>به دستیار هوشمند Antigravity (AGY) خوش آمدید!</b>\n\n"
f"دسترسی شما با لینک دعوت معتبر با موفقیت فعال شد.\n"
f"برای شروع گفتگو و کدنویسی، از کنترل‌پنل زیر استفاده کنید:"
)
await update.message.reply_html(welcome_msg)
else:
err_msg = (
f"❌ <b>خطا در استفاده از لینک دعوت:</b>\n"
f"{msg}\n\n"
f"در صورت نیاز می‌توانید با دکمه زیر درخواست دسترسی ارسال کنید:"
)
keyboard = [[InlineKeyboardButton("📩 ارسال درخواست دسترسی به مدیر", callback_data="req_access")]]
await update.message.reply_html(err_msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
text, markup = build_main_dashboard(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /help
@check_auth
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if is_fa:
text = (
f"📖 <b>راهنمای جامع ربات Antigravity (AGY)</b>\n\n"
f"<b>📁 مدیریت چند پروژه‌ای (Projects):</b>\n"
f"• <code>/projects</code> یا <code>/project</code> - پنل تعاملی مدیریت و انتخاب پروژه‌ها\n"
f"• <code>/newproject &lt;name&gt;</code> - ساخت و فعال‌سازی پروژه اختصاصی با پوشه و حافظه مستقل\n"
f"• <code>/switch &lt;name&gt;</code> - سوییچ بین پروژه‌ها\n"
f"• <code>/delproject &lt;name&gt;</code> - حذف یک پروژه\n"
f"• <code>/renameproject &lt;قدیم&gt; &lt;جدید&gt;</code> - تغییر نام پروژه\n"
f"• <code>/backup [نام]</code> - تهیه فایل Zip و ارسال بکاپ پروژه در پارت‌های ۵۰ مگابایتی\n\n"
f"<b>🐙 کنترل نسخه و لغو تغییرات (Git &amp; Undo):</b>\n"
f"• <code>/git</code> یا <code>/repo</code> - داشبورد مدیریت گیت و مشاهده وضعیت مخزن Gitea\n"
f"• <code>/sync</code> - همگام‌سازی سریع با مخزن (Pull &amp; Push)\n"
f"• <code>/commit [پیام]</code> - ثبت و ارسال دستی کامیت\n"
f"• <code>/undo [hash]</code> یا <code>/revert</code> - لغو امن آخرین تغییر (HEAD) یا یک کامیت مشخص از گذشته\n\n"
f"<b>⏰ زمان‌بندی تسک‌ها (Scheduled Tasks &amp; Cron):</b>\n"
f"• <code>/tasks</code> یا <code>/schedule</code> - کنترل‌پنل تعاملی تسک‌های زمان‌بندی شده\n"
f"• <code>/schedule &lt;زمان&gt; | &lt;پرامپت/دستور&gt;</code> - ایجاد و زمان‌بندی تسک جدید\n"
f" <i>مثال‌ها:</i>\n"
f" • <code>/schedule every 1h | سلامت سرور را چک کن</code>\n"
f" • <code>/schedule in 15m | گزارش وضعیت پروژه</code>\n"
f" • <code>/schedule 0 9 * * * | خلاصه وظایف روزانه</code>\n"
f" • <code>/schedule every 2h | cmd: git pull &amp;&amp; npm test</code>\n"
f"• <code>/tasks list</code> - مشاهده لیست کامل تسک‌ها\n"
f"• <code>/tasks run &lt;id&gt;</code> - اجرای فوری و دستی یک تسک\n"
f"• <code>/tasks pause &lt;id&gt;</code> / <code>/tasks resume &lt;id&gt;</code> - توقف یا فعال‌سازی\n"
f"• <code>/tasks del &lt;id&gt;</code> - حذف تسک زمان‌بندی شده\n\n"
f"<b>🤝 اشتراک‌گذاری پروژه (Project Sharing):</b>\n"
f"• <code>/share &lt;user_id&gt;</code> - اشتراک‌گذاری پروژه فعال با کاربر دیگر\n"
f"• <code>/share &lt;نام_پروژه&gt; &lt;user_id&gt;</code> - اشتراک‌گذاری پروژه خاص با کاربر\n"
f"• <code>/unshare &lt;user_id&gt;</code> - لغو دسترسی کاربر به پروژه فعال\n"
f"• <code>/shared</code> - مشاهده لیست پروژه‌های اشتراکی\n\n"
f"<b>💬 مدیریت گفتگو و تاریخچه:</b>\n"
f"• <code>/conversations</code> - مشاهده لیست تعاملی، سوییچ و حذف گفتگوها\n"
f"• <code>/switchconv &lt;شماره یا شناسه&gt;</code> - سوییچ سریع به گفتگوی خاص\n"
f"• <code>/delconv &lt;شماره یا شناسه&gt;</code> - حذف گفتگوی خاص یا گفتگوی فعال\n"
f"• <code>/clearconvs</code> - پاکسازی و حذف تمام گفتگوهای پروژه فعال\n"
f"• <code>/lastconv</code> - باز کردن و بازیابی آخرین گفتگو / گفتگوی قبلی\n"
f"• <code>/new</code> یا <code>/reset</code> - شروع گفتگوی تازه (با امکان بازگشت)\n"
f"• <code>/status</code> - مشاهده وضعیت مدل، پروژه، حافظه سرور و دیسک\n"
f"• <code>/server</code> یا <code>/hardware</code> - وضعیت لحظه‌ای رم (RAM)، سی‌پی‌یو (CPU) و حافظه داخلی سرور\n"
f"• <code>/usage</code> یا <code>/quota</code> - استعلام لحظه‌ای سهمیه و مصرف AGY\n"
f"• <code>/last</code> - مشاهده آخرین خروجی و ابزارهای اجرا شده\n"
f"• <code>/context</code> - نمایش درصد اشغال کانتکست و تعداد توکن‌ها\n"
f"• <code>/cancel</code> - لغو پردازش یا دستور در حال اجرا\n"
f"• <code>/restart</code> - ری‌استارت سرویس ربات\n\n"
f"<b>⚙️ تنظیمات:</b>\n"
f"• <code>/model</code> - انتخاب مدل هوش مصنوعی\n"
f"• <code>/lang [زبان]</code> - تغییر زبان پاسخ‌دهی (مثلاً <code>/lang fa</code>)\n"
f"• <code>/effort [low|med|high]</code> - تنظیم عمق تفکر استدلالی\n"
f"<b>🧠 حافظه بلندمدت هوش مصنوعی (AI Memory):</b>\n"
f"• <code>/memory</code> - کنترل‌پنل تعاملی حافظه هوشمند، دکمه‌های افزودن، مشاهده و پاک‌سازی خاطرات\n"
f"• <code>/memory add [project|user|global] کلید | متن</code> - افزودن مستقیم خاطره جدید\n"
f"• <code>/memory global</code> - مشاهده قوانین و خاطرات عمومی کل سیستم\n"
f"• <code>/memory private</code> - مشاهده خاطرات اختصاصی و شخصی شما\n"
f"• <code>/memory clear</code> - پاک‌سازی خاطرات اختصاصی شما\n"
f"• <i>امکان افزودن با ۱ کلیک از طریق دکمه «➕ افزودن خاطره» در پنل حافظه و منوی پروژه‌ها فراهم است.</i>\n\n"
f"<b>👑 مدیریت کاربران و دعوت‌ها (ویژه مدیر):</b>\n"
f"• <code>/users</code> - پنل تعاملی مدیریت کاربران و لیست مجاز\n"
f"• <code>/invite &lt;user_id&gt;</code> - دعوت و اعطای مستقیم دسترسی به کاربر\n"
f"• <code>/invitelink</code> یا <code>/invite link</code> - ساخت لینک دعوت جدید اختصاصی\n"
f"• <code>/uninvite &lt;user_id&gt;</code> - لغو دسترسی کاربر\n\n"
f"<b>🤖 کنترل خودکار ربات از طریق هوش مصنوعی (AI Actions):</b>\n"
f"• شما می‌توانید به زبان محاوره‌ای از هوش مصنوعی بخواهید تا تنظیمات و عملکرد ربات را کنترل کند! مثلاً:\n"
f" • «مدل این پروژم رو بذار روی کلود sonnet»\n"
f" • «یک پروژه جدید به اسم فروشگاه بساز و سوییچ کن روش»\n"
f" • «سطح تفکر مدل رو high کن»\n"
f" • «پورت ۳۰۰۰ رو روی ساب‌دامین app بالا بیار (Caddy)»\n"
f" • «فایل بکاپ پروژم رو برام بفرست»\n"
f" • «یک تسک بساز هر روز ساعت ۹ لاگ‌ها رو برام بفرسته»\n"
f" • «گفتگوی قبلی رو باز کن» یا «چت رو ریست کن»\n\n"
f"⚡ <i>هر کاربر در دایرکتوری ایزوله خود فعالیت می‌کند و پروژه‌ها به صورت مستقل مدیریت می‌شوند.</i>"
)
keyboard = [
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("⏰ زمان‌بندی تسک‌ها", callback_data="btn_tasks_menu"),
],
[
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("🧠 انتخاب مدل", callback_data="proj_model_menu"),
],
[
InlineKeyboardButton("📈 سهمیه و مصرف", callback_data="btn_usage_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 بستن", callback_data="proj_close"),
],
]
else:
text = (
f"📖 <b>Antigravity (AGY) Bot Reference</b>\n\n"
f"<b>📁 Multi-Project Management:</b>\n"
f"• <code>/projects</code> or <code>/project</code> - Interactive Project Manager panel\n"
f"• <code>/newproject &lt;name&gt;</code> - Create & activate an isolated project\n"
f"• <code>/switch &lt;name&gt;</code> - Switch active project\n"
f"• <code>/delproject &lt;name&gt;</code> - Delete a project\n"
f"• <code>/renameproject &lt;old&gt; &lt;new&gt;</code> - Rename a project\n"
f"• <code>/backup [name]</code> - Zip and export project in 50MB files to Telegram\n\n"
f"<b>⏰ Scheduled Tasks &amp; Timings:</b>\n"
f"• <code>/tasks</code> or <code>/schedule</code> - Interactive Scheduled Tasks Manager\n"
f"• <code>/schedule &lt;timing&gt; | &lt;prompt/cmd&gt;</code> - Schedule a task\n"
f" <i>Examples:</i>\n"
f" • <code>/schedule every 1h | check server health</code>\n"
f" • <code>/schedule in 15m | check project status</code>\n"
f" • <code>/schedule 0 9 * * * | prepare morning summary</code>\n"
f" • <code>/schedule every 2h | cmd: git pull &amp;&amp; npm test</code>\n"
f"• <code>/tasks list</code> - List all tasks\n"
f"• <code>/tasks run &lt;id&gt;</code> - Run a task immediately\n"
f"• <code>/tasks pause &lt;id&gt;</code> / <code>/tasks resume &lt;id&gt;</code> - Pause/Resume\n"
f"• <code>/tasks del &lt;id&gt;</code> - Delete scheduled task\n\n"
f"<b>🤝 Project Sharing:</b>\n"
f"• <code>/share &lt;user_id&gt;</code> - Share active project with a user\n"
f"• <code>/share &lt;proj_name&gt; &lt;user_id&gt;</code> - Share specific project\n"
f"• <code>/unshare &lt;user_id&gt;</code> - Revoke user access\n"
f"• <code>/shared</code> - View shared projects list\n\n"
f"<b>💬 Session & Conversation History:</b>\n"
f"• <code>/conversations</code> - View interactive history, switch, and delete\n"
f"• <code>/switchconv &lt;num|id&gt;</code> - Quick switch to a specific conversation\n"
f"• <code>/delconv &lt;num|id&gt;</code> - Delete a specific or active conversation\n"
f"• <code>/clearconvs</code> - Clear all conversations for active project\n"
f"• <code>/lastconv</code> - Reopen last or previous conversation\n"
f"• <code>/new</code> or <code>/reset</code> - Reset conversation in active project\n"
f"• <code>/status</code> - View current model, project, memory & disk status\n"
f"• <code>/usage</code> or <code>/quota</code> - Check real-time AGY usage & limits\n"
f"• <code>/last</code> - View last AI output & executed tools\n"
f"• <code>/context</code> - View token stats & context length\n"
f"• <code>/cancel</code> - Cancel active generation or task\n"
f"• <code>/restart</code> - Restart bot service\n\n"
f"<b>⚙️ Configuration:</b>\n"
f"• <code>/model</code> - Select AI model\n"
f"• <code>/lang [code|name]</code> - Change response language\n"
f"• <code>/effort [low|med|high]</code> - Set reasoning depth\n"
f"• <code>/workspace [path]</code> - Set working directory for active project\n\n"
f"<b>👑 Admin User &amp; Invitation Management:</b>\n"
f"• <code>/users</code> - Interactive User Whitelist &amp; Invite Panel\n"
f"• <code>/invite &lt;user_id&gt;</code> - Directly invite and authorize a user\n"
f"• <code>/invitelink</code> or <code>/invite link</code> - Generate a new invite link\n"
f"• <code>/uninvite &lt;user_id&gt;</code> - Revoke user authorization\n\n"
f"<b>🤖 Natural Language AI Bot Actions:</b>\n"
f"• Ask the AI directly in chat to perform any bot operation for you! Examples:\n"
f" • 'Switch model to Claude Sonnet for this project'\n"
f" • 'Create a new project named store and switch to it'\n"
f" • 'Set reasoning effort to high'\n"
f" • 'Expose port 3000 on subdomain app (Caddy reverse proxy)'\n"
f" • 'Send me a backup zip of this project'\n"
f" • 'Schedule a daily task at 9am to check server logs'\n"
f" • 'Reset conversation context' or 'Open previous chat'\n\n"
f"⚡ <i>Each project maintains its own isolated workspace, conversation ID, and token context!</i>"
)
keyboard = [
[
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("⏰ Tasks", callback_data="btn_tasks_menu"),
],
[
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🧠 AI Model", callback_data="proj_model_menu"),
],
[
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
],
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /projects or /project
@check_auth
async def projects_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
if context.args:
subcmd = context.args[0].lower()
if subcmd in ("new", "create", "add") and len(context.args) > 1:
await new_project_command(update, context)
return
elif subcmd in ("switch", "use", "select") and len(context.args) > 1:
await switch_project_command(update, context)
return
elif subcmd in ("del", "delete", "remove") and len(context.args) > 1:
await delete_project_command(update, context)
return
elif subcmd in ("share", "sharing") and len(context.args) > 1:
await share_command(update, context)
return
text, markup = build_projects_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /newproject <name>
@check_auth
async def new_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
args = context.args or []
if args and args[0].lower() in ("new", "create", "add"):
args = args[1:]
if not args:
if is_fa:
msg = (
"️ <b>راهنمای ساخت پروژه جدید:</b>\n\n"
"دستور را به این صورت ارسال کنید:\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b>\n"
"• <code>/newproject my-project</code>\n"
"• <code>/newproject webapp</code>"
)
else:
msg = (
"️ <b>How to create a new project:</b>\n\n"
"Send:\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Examples:</b>\n"
"• <code>/newproject my-project</code>\n"
"• <code>/newproject webapp</code>"
)
await update.message.reply_html(msg)
return
name = args[0]
workspace = args[1] if (len(args) > 1 and settings.is_admin(chat_id)) else None
proj = await session_manager.create_project(chat_id=chat_id, name=name, workspace=workspace)
if is_fa:
text = (
f"🎉 <b>پروژه جدید با موفقیت ایجاد و فعال شد!</b>\n\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>\n"
f"• ⚡ <b>استدلال:</b> <code>{proj.effort}</code>\n\n"
f"اکنون می‌توانید پیام‌ها، فایل‌ها و دستورات خود را ارسال کنید."
)
else:
text = (
f"🎉 <b>New Project Created & Activated!</b>\n\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>\n"
f"• ⚡ <b>Effort:</b> <code>{proj.effort}</code>\n\n"
f"You can now send prompts for this project."
)
await update.message.reply_html(text)
# Command: /switch <name> or /switchproject <name>
@check_auth
async def switch_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
args = context.args or []
if args and args[0].lower() in ("switch", "use", "select"):
args = args[1:]
if not args:
text, markup = build_projects_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
target_name = args[0]
proj = await session_manager.switch_project(chat_id, target_name)
if proj:
owner_str = f" (مالک: <code>{proj.owner_id}</code>)" if proj.owner_id and proj.owner_id != chat_id else ""
if is_fa:
msg = (
f"🟢 <b>پروژه فعال تغییر یافت!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>{owner_str}\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(proj.workspace)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{proj.model}</code>\n"
f"• 💬 <b>وضعیت مکالمه:</b> <code>{proj.conversation_id or 'جدید'}</code>"
)
else:
msg = (
f"🟢 <b>Switched Active Project!</b>\n\n"
f"• 📁 <b>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>\n"
f"• 💬 <b>Conversation:</b> <code>{proj.conversation_id or 'New'}</code>"
)
await update.message.reply_html(msg)
else:
accessible = session_manager.get_all_accessible_projects(chat_id)
avail = ", ".join([f"<code>{p.name}</code>" for p in accessible.values()]) if accessible else "(هیچ پروژه‌ای وجود ندارد)"
if is_fa:
msg = f"❌ پروژه <code>{escape_html(target_name)}</code> یافت نشد یا شما به آن دسترسی ندارید.\nپروژه‌های در دسترس شما: {avail}"
else:
msg = f"❌ Project <code>{escape_html(target_name)}</code> not found or access denied.\nAvailable projects: {avail}"
await update.message.reply_html(msg)
# Command: /delproject <name> or /deleteproject <name>
@check_auth
async def delete_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
own_projs = session_manager.get_user_projects(chat_id)
args = context.args or []
if args and args[0].lower() in ("del", "delete", "remove"):
args = args[1:]
if not args:
if not own_projs:
msg = "⚠️ شما هیچ پروژه‌ای برای حذف ندارید." if is_fa else "⚠️ You have no projects to delete."
await update.message.reply_html(msg)
return
keyboard = []
for name in own_projs.keys():
keyboard.append([InlineKeyboardButton(f"🗑️ حذف {name}" if is_fa else f"🗑️ Delete {name}", callback_data=f"proj_del_confirm:{name}")])
keyboard.append([InlineKeyboardButton("🔙 بازگشت به پروژه‌ها" if is_fa else "🔙 Back", callback_data="proj_menu")])
msg = "🗑️ <b>پروژه‌ای که می‌خواهید حذف شود را انتخاب کنید:</b>" if is_fa else "🗑️ <b>Select project to delete:</b>"
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
target_name = args[0]
own_projs = session_manager.get_user_projects(chat_id)
matched = None
for k in own_projs.keys():
if k.lower() == target_name.lower():
matched = k
break
if not matched:
msg = f"❌ پروژه <code>{escape_html(target_name)}</code> در لیست پروژه‌های شما یافت نشد." if is_fa else f"❌ Project <code>{escape_html(target_name)}</code> not found in your projects."
await update.message.reply_html(msg)
return
text = (
f"⚠️ <b>تأیید حذف کامل و هوشمند پروژه:</b> <code>{escape_html(matched)}</code>\n\n"
f"آیا از حذف کامل این پروژه و تمامی منابع متصل به آن اطمینان دارید؟\n\n"
f"<b>منابعی که بررسی و پاکسازی می‌شوند:</b>\n"
f"• 🗑️ کلیه فایل‌ها و دایرکتوری پروژه در سرور\n"
f"• 🐙 مخزن اختصاصی پروژه در سرور گیت (Gitea)\n"
f"• 🌐 ساب‌دامین و پراکسی فعال در Caddy (در صورت وجود)\n"
f"• ⏰ تسک‌ها و کران‌جاب‌های زمان‌بندی‌شده مربوط به پروژه\n"
f"• 🛑 فرآیندها، پورت‌ها و سرویس‌های فعال پروژه"
if is_fa else
f"⚠️ <b>Confirm Full Teardown & Deletion:</b> <code>{escape_html(matched)}</code>\n\n"
f"Are you sure you want to permanently delete this project and release all its resources?\n\n"
f"<b>Resources to be cleaned up:</b>\n"
f"• 🗑️ Workspace files and project directory\n"
f"• 🐙 Dedicated repository on Gitea server\n"
f"• 🌐 Subdomain & Caddy reverse proxy (if any)\n"
f"• ⏰ Scheduled tasks and cron jobs for this project\n"
f"• 🛑 Running processes, ports and services"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، حذف و پاکسازی کامل" if is_fa else "💥 Yes, Full Teardown", callback_data=f"proj_del_execute:{matched}"),
InlineKeyboardButton("❌ خیر، انصراف" if is_fa else "❌ No, Cancel", callback_data="proj_menu"),
]
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /renameproject <old> <new>
@check_auth
async def rename_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if len(context.args) < 2:
msg = "استفاده: <code>/renameproject &lt;نام_قدیمی&gt; &lt;نام_جدید&gt;</code>" if is_fa else "Usage: <code>/renameproject &lt;old_name&gt; &lt;new_name&gt;</code>"
await update.message.reply_html(msg)
return
old_name, new_name = context.args[0], context.args[1]
renamed, msg = await session_manager.rename_project(chat_id, old_name, new_name)
await update.message.reply_html(msg)
# Command: /share [project_name] <user_id>
@check_auth
async def share_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>You don't have any projects yet!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
args = context.args or []
if not args:
text, markup = build_sharing_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
target_uid = None
target_pname = None
if len(args) == 1:
if args[0].isdigit():
target_uid = int(args[0])
target_pname = None
else:
msg = (
"️ <b>راهنمای دستور اشتراک‌گذاری (/share):</b>\n\n"
"• <code>/share &lt;user_id&gt;</code> (اشتراک پروژه فعال)\n"
"• <code>/share &lt;نام_پروژه&gt; &lt;user_id&gt;</code>\n\n"
"<b>مثال:</b> <code>/share 123456789</code>"
if is_fa else
"️ <b>Share Command Usage:</b>\n\n"
"• <code>/share &lt;user_id&gt;</code> (Active project)\n"
"• <code>/share &lt;project_name&gt; &lt;user_id&gt;</code>\n\n"
"<b>Example:</b> <code>/share 123456789</code>"
)
await update.message.reply_html(msg)
return
else:
if args[-1].isdigit():
target_uid = int(args[-1])
target_pname = " ".join(args[:-1]).strip()
else:
msg = "❌ شناسه کاربری (User ID) باید یک مقدار عددی باشد." if is_fa else "❌ User ID must be numeric."
await update.message.reply_html(msg)
return
success, message_text, proj = await session_manager.share_project(
owner_chat_id=chat_id,
target_user_id=target_uid,
project_name=target_pname,
)
await update.message.reply_html(message_text)
# Command: /unshare [project_name] <user_id>
@check_auth
async def unshare_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هیچ پروژه‌ای ندارید." if is_fa else "⚠️ You have no projects."
await update.message.reply_html(msg)
return
args = context.args or []
if not args:
text, markup = build_sharing_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
target_uid = None
target_pname = None
if len(args) == 1:
if args[0].isdigit():
target_uid = int(args[0])
target_pname = None
else:
msg = "❌ لطفاً شناسه عددی کاربر را وارد کنید: <code>/unshare &lt;user_id&gt;</code>" if is_fa else "❌ Please provide a numeric user ID: <code>/unshare &lt;user_id&gt;</code>"
await update.message.reply_html(msg)
return
else:
if args[-1].isdigit():
target_uid = int(args[-1])
target_pname = " ".join(args[:-1]).strip()
else:
msg = "❌ شناسه کاربری باید عددی باشد." if is_fa else "❌ User ID must be numeric."
await update.message.reply_html(msg)
return
success, message_text, proj = await session_manager.unshare_project(
owner_chat_id=chat_id,
target_user_id=target_uid,
project_name=target_pname,
)
await update.message.reply_html(message_text)
# Command: /shared
@check_auth
async def shared_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
own_projs = session_manager.get_user_projects(chat_id)
shared_with_me = session_manager.get_shared_projects(chat_id)
shared_by_me = {}
for name, p in own_projs.items():
if p.shared_with:
shared_by_me[name] = p
if is_fa:
text = "🤝 <b>لیست پروژه‌های اشتراکی (Shared Projects)</b>\n\n"
if not shared_by_me and not shared_with_me:
text += "<i>در حال حاضر هیچ پروژه اشتراکی یافت نشد.</i>\n\n"
text += "💡 برای اشتراک‌گذاری پروژه فعال با کاربر دیگر:\n<code>/share &lt;user_id&gt;</code>"
else:
if shared_by_me:
text += f"📤 <b>پروژه‌های شما که با دیگران اشتراک گذاشته‌اید ({len(shared_by_me)}):</b>\n"
for name, p in shared_by_me.items():
uids = ", ".join([f"<code>{uid}</code>" for uid in p.shared_with])
text += f"• 📁 <b>{escape_html(name)}</b> ➔ {uids}\n"
text += "\n"
if shared_with_me:
text += f"📥 <b>پروژه‌های دیگران که با شما به اشتراک گذاشته شده‌اند ({len(shared_with_me)}):</b>\n"
for name, p in shared_with_me.items():
text += f"• 👥 <b>{escape_html(p.name)}</b> (مالک: <code>{p.owner_id}</code>)\n"
text += "\n"
text += "💡 <i>برای سوییچ به هر پروژه:</i> <code>/switch &lt;نام_پروژه&gt;</code>"
else:
text = "🤝 <b>Shared Projects</b>\n\n"
if not shared_by_me and not shared_with_me:
text += "<i>No shared projects found.</i>\n\n"
text += "💡 To share your active project: <code>/share &lt;user_id&gt;</code>"
else:
if shared_by_me:
text += f"📤 <b>Projects you shared with others ({len(shared_by_me)}):</b>\n"
for name, p in shared_by_me.items():
uids = ", ".join([f"<code>{uid}</code>" for uid in p.shared_with])
text += f"• 📁 <b>{escape_html(name)}</b> ➔ {uids}\n"
text += "\n"
if shared_with_me:
text += f"📥 <b>Projects shared with you ({len(shared_with_me)}):</b>\n"
for name, p in shared_with_me.items():
text += f"• 👥 <b>{escape_html(p.name)}</b> (Owner: <code>{p.owner_id}</code>)\n"
text += "\n"
text += "💡 <i>To switch to a project:</i> <code>/switch &lt;project_name&gt;</code>"
await update.message.reply_html(text)
# Command: /last or /output
@check_auth
async def last_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
output_data = get_conversation_last_output(curr_proj.conversation_id) if curr_proj.conversation_id else {}
ai_text = output_data.get("text") or curr_proj.last_response
tools_used = output_data.get("tools", [])
if not ai_text and not tools_used:
msg = f"️ <i>هیچ خروجی یا پاسخی در پروژه <b>{escape_html(curr_proj.name)}</b> ثبت نشده است.</i>" if is_fa else f"️ <i>No previous AI output found for project <b>{escape_html(curr_proj.name)}</b>.</i>"
await update.message.reply_html(msg)
return
if ai_text and ai_text.strip():
header = f"📋 <b>آخرین پاسخ هوش مصنوعی (پروژه: {escape_html(curr_proj.name)}):</b>\n\n" if is_fa else f"📋 <b>Last AI Output (Project: {escape_html(curr_proj.name)}):</b>\n\n"
formatted = markdown_to_telegram_html(ai_text)
full_msg = header + formatted
else:
tools_header = f"🛠 <b>آخرین ابزارهای اجرا شده (پروژه: {escape_html(curr_proj.name)}):</b>\n" if is_fa else f"🛠 <b>Last executed tools (Project: {escape_html(curr_proj.name)}):</b>\n"
tools_list = "\n".join([f"• <code>{escape_html(t)}</code>" for t in tools_used[-8:]])
full_msg = tools_header + tools_list
chunks = split_message(full_msg, max_length=settings.max_message_length)
for chunk in chunks:
await update.message.reply_html(chunk, disable_web_page_preview=True)
async def graceful_bot_restart(delay_after_done: float = 1.5, max_wait: float = 180.0):
"""
Waits for all in-progress AI operations, agent tasks, and background processes
to finish completely before restarting the systemd bot service.
"""
logger.info("Graceful restart requested. Waiting for in-progress operations to finish...")
start_wait = time.time()
while time.time() - start_wait < max_wait:
in_progress = any(sess.turn_in_progress for sess in session_manager.sessions.values())
has_tasks = len(session_manager.active_tasks) > 0
has_procs = len(session_manager.active_procs) > 0
if not in_progress and not has_tasks and not has_procs:
logger.info("All active tasks completed. Executing graceful restart now.")
break
await asyncio.sleep(1.5)
await asyncio.sleep(delay_after_done)
os.system("systemctl restart --no-block agy-telegram-bot.service &")
# Command: /upload or /drop
@check_auth
async def upload_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ You have no active projects."
await update.message.reply_html(msg)
return
p_name = curr_proj.name
token = create_upload_token(chat_id, p_name)
upload_url = get_upload_url(token)
if is_fa:
msg = (
f"📤 <b>صفحه آپلود فایل اختصاصی پروژه: <code>{escape_html(p_name)}</code></b>\n\n"
f"با این لینک می‌توانید فایل‌های حجیم و پروژه‌های خود (تا سقف ۲ گیگابایت) را بدون محدودیت تلگرام آپلود کنید:\n\n"
f"🔗 <b>لینک اختصاصی:</b>\n"
f"{upload_url}\n\n"
f"💡 <i>پشتیبانی از اکسترکت خودکار فایل‌های ZIP/TAR مستقیم در پوشه پروژه!</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 ورود به صفحه آپلود", url=upload_url)],
[InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu")],
]
else:
msg = (
f"📤 <b>Web Upload Portal for <code>{escape_html(p_name)}</code></b>\n\n"
f"🔗 <b>Upload Link (up to 2GB):</b>\n"
f"{upload_url}"
)
keyboard = [
[InlineKeyboardButton("🌐 Open Upload Portal", url=upload_url)],
[InlineKeyboardButton("📁 Projects", callback_data="proj_menu")],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
async def process_agent_turn_by_chat_id(
app: Application,
chat_id: int,
prompt: str,
initial_status_text: Optional[str] = None,
):
"""Triggers an agent turn from external event (like Web Uploader or AI Button)."""
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
return
if not auth_manager.has_custom_account(chat_id):
warn_text = (
"⚠️ <b>حساب کاربری هوش مصنوعی شما متصل نیست!</b>\n\n"
"در این ربات حساب‌های اشتراکی غیرفعال هستند. لطفاً ابتدا از طریق دستور <code>/login</code> با حساب اختصاصی خود وارد شوید."
if is_fa else
"⚠️ <b>AGY Account Not Connected!</b>\n\nShared accounts are disabled. Please connect your personal Google account via /login."
)
keyboard = [[InlineKeyboardButton("🔑 ورود به حساب گوگل (/login)" if is_fa else "🔑 Log in with Google", callback_data="btn_account_start_login")]]
await app.bot.send_message(chat_id=chat_id, text=warn_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
return
# Create dummy update/context wrapper or send initial status message
if initial_status_text:
initial_status = initial_status_text
else:
model_to_use = getattr(curr_proj, "model", None) or getattr(session, "model", None) or settings.default_model or "gemini-3.7-flash-auto"
effort_to_use = getattr(curr_proj, "reasoning_effort", None) or getattr(session, "reasoning_effort", None)
model_display = get_model_display_name(model_to_use, effort_to_use)
curr_cid = getattr(curr_proj, "conversation_id", None)
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
proj_label = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
status_lines = [f"🤔 <b>{escape_html(model_display)}</b>"]
if conv_title:
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
initial_status = f"{proj_label}\n\n" + "\n".join(status_lines)
status_msg = await app.bot.send_message(
chat_id=chat_id,
text=initial_status,
parse_mode=constants.ParseMode.HTML,
reply_markup=get_stop_button(session.language),
)
class DummyUpdate:
def __init__(self, c_id, s_msg):
self.effective_chat = type("Chat", (), {"id": c_id})()
self.effective_user = type("User", (), {"id": c_id})()
self.effective_message = s_msg
self.message = s_msg
class DummyContext:
def __init__(self, application):
self.application = application
self.bot = application.bot
dummy_update = DummyUpdate(chat_id, status_msg)
dummy_context = DummyContext(app)
await process_agent_turn(dummy_update, dummy_context, prompt, status_msg_to_reuse=status_msg)
# Command: /restart
@check_auth
async def restart_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if not settings.is_admin(user_id):
await update.message.reply_html("⛔ /restart is restricted to Bot Administrators.")
return
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
in_progress = any(sess.turn_in_progress for sess in session_manager.sessions.values()) or len(session_manager.active_tasks) > 0
if in_progress:
msg = (
"⏳ <b>عملیات هوش مصنوعی در حال اجرا است.</b>\n\n"
"ربات منتظر اتمام کامل فرآیند پردازش می‌ماند و بلافاصله پس از تکمیل آن به صورت خودکار و بدون وقفه ری‌استارت خواهد شد..."
if is_fa else
"⏳ <b>AI operations are in progress.</b>\n\n"
"Bot will wait for all active operations to complete, then restart gracefully..."
)
else:
msg = (
"🔄 <b>در حال راه‌اندازی مجدد سرویس ربات...</b>\n\nتغییرات در چند ثانیه آینده اعمال خواهند شد."
if is_fa else
"🔄 <b>Restarting AGY Bot service...</b>\n\nChanges will take effect in a few seconds."
)
await update.message.reply_html(msg)
asyncio.create_task(graceful_bot_restart())
# Command: /exec
@check_auth
async def exec_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
if not settings.is_admin(user_id):
await update.message.reply_html("⛔ /exec is restricted to Bot Administrators.")
return
cmd_text = " ".join(context.args) if context.args else ""
if not cmd_text:
await update.message.reply_html("Usage: <code>/exec &lt;shell command&gt;</code>")
return
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
ws_cwd = curr_proj.workspace if curr_proj else settings.default_workspace
status_msg = await update.message.reply_html("⏳ Executing command...")
try:
proc = await asyncio.create_subprocess_shell(
cmd_text,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
limit=100 * 1024 * 1024,
cwd=ws_cwd,
)
stdout, stderr = await proc.communicate()
out = stdout.decode("utf-8", errors="replace")
err = stderr.decode("utf-8", errors="replace")
combined = (out + ("\nSTDERR:\n" + err if err else "")).strip()
if not combined:
combined = "(No output, return code 0)"
escaped = escape_html(combined)
for chunk in split_message(f"🖥️ <b>Command Output:</b>\n<pre>{escaped}</pre>"):
await update.message.reply_html(chunk)
await status_msg.delete()
except Exception as e:
await status_msg.edit_text(f"❌ Execution failed: {e}")
# Helper: Usage & Stats Action Buttons
def get_usage_buttons(lang: str = "fa") -> InlineKeyboardMarkup:
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
restart_label = "🔄 گفتگوی جدید" if is_fa else "🔄 New Chat"
compact_label = "🗜️ فشرده‌سازی" if is_fa else "🗜️ Compact"
conv_label = "📜 سشن‌ها / گفتگوها" if is_fa else "📜 Conversations"
last_conv_label = "⏮️ گفتگوی قبلی" if is_fa else "⏮️ Previous Chat"
keyboard = [
[
InlineKeyboardButton(compact_label, callback_data="btn_compact"),
InlineKeyboardButton(restart_label, callback_data="btn_restart"),
],
[
InlineKeyboardButton(last_conv_label, callback_data="btn_conv_last"),
InlineKeyboardButton(conv_label, callback_data="btn_conv_menu"),
]
]
return InlineKeyboardMarkup(keyboard)
# Helper: Stop Action Button for active tasks
def get_stop_button(lang: str = "fa") -> InlineKeyboardMarkup:
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
stop_label = "🛑 توقف" if is_fa else "🛑 Stop"
keyboard = [
[
InlineKeyboardButton(stop_label, callback_data="btn_stop"),
]
]
return InlineKeyboardMarkup(keyboard)
# Command: /new or /reset
@check_auth
async def new_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>No active project found!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
await session_manager.reset_session(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
if is_fa:
msg = (
f"🔄 <b>گفتگوی جدید آغاز شد!</b>\n\n"
f"حافظه و تاریخچه گفتگوی قبلی برای پروژه <code>{escape_html(curr_proj.name)}</code> بازنشانی گردید.\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>در صورت نیاز به بازگشت به گفتگوی قبلی، از دکمه زیر یا دستور <code>/lastconv</code> استفاده نمایید:</i>"
)
keyboard = [
[
InlineKeyboardButton("⏮️ باز کردن گفتگوی قبلی", callback_data="btn_conv_last"),
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
msg = (
f"🔄 <b>New Conversation Started!</b>\n\n"
f"Conversation context reset for project <code>{escape_html(curr_proj.name)}</code>.\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>To return to the previous conversation, use the button below or <code>/lastconv</code>:</i>"
)
keyboard = [
[
InlineKeyboardButton("⏮️ Reopen Previous Chat", callback_data="btn_conv_last"),
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /compact or /compress
@check_auth
async def compact_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ ابتدا با دستور <code>/newproject</code> یک پروژه بسازید." if is_fa else "⚠️ Please create a project first."
await update.message.reply_html(msg)
return
toks = get_conversation_context_tokens(curr_proj.conversation_id)
curr_proj.last_context_length = toks
session.last_context_length = toks
session_manager.save()
model_lower = (curr_proj.model or "").lower()
if "3.1-pro" in model_lower or "pro" in model_lower:
max_ctx = 2_000_000
elif "claude" in model_lower:
max_ctx = 200_000
elif "gpt" in model_lower:
max_ctx = 128_000
else:
max_ctx = 1_000_000
pct = (toks / max_ctx) * 100 if max_ctx else 0
pct_str = f"{pct:.1f}%"
free_pct = f"{max(0.0, 100.0 - pct):.1f}%"
if is_fa:
msg = (
f"🗜️ <b>کانتکست با موفقیت فشرده و بهینه‌سازی شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>طول کانتکست فعال:</b> <code>{toks:,} توکن</code>\n"
f"• 🎯 <b>سقف پنجره:</b> <code>{max_ctx:,} توکن</code>\n"
f"• 📊 <b>میزان اشغال پنجره (CL):</b> <code>{pct_str}</code> (فضای آزاد: <code>{free_pct}</code>)\n\n"
f"✅ حافظه گفتگو بهینه‌سازی شد و برای ادامه آماده است."
)
keyboard = [
[
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
msg = (
f"🗜️ <b>Context Compacted & Optimized!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>Active Context:</b> <code>{toks:,} tokens</code>\n"
f"• 🎯 <b>Max Window:</b> <code>{max_ctx:,} tokens</code>\n"
f"• 📊 <b>Usage (CL):</b> <code>{pct_str}</code> (Free: <code>{free_pct}</code>)\n\n"
f"✅ Context memory is optimized and ready."
)
keyboard = [
[
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /lastconv or /lastconversation or /openlast or /resume or /open_last
@check_auth
async def last_conversation_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
success, msg_text, conv_id, meta = session_manager.reopen_last_conversation(chat_id)
if not success:
await update.message.reply_html(msg_text)
return
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
turns = meta.get("turns_count", 0)
if is_fa:
text = (
f"⏮️ <b>آخرین گفتگو با موفقیت بازیابی و باز شد!</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>شناسه مکالمه:</b> <code>{conv_id}</code>\n"
f"• 🔢 <b>تعداد نوبت‌ها:</b> <code>{turns} نوبت</code>\n"
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>اکنون می‌توانید پیام جدید خود را ارسال کنید تا دقیقاً در ادامه همین گفتگو پاسخ داده شود.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
],
[
InlineKeyboardButton("📊 وضعیت سیستم", callback_data="btn_status_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"⏮️ <b>Last Conversation Reopened Successfully!</b>\n\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Conversation ID:</b> <code>{conv_id}</code>\n"
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>Send your next message to continue this conversation.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
],
[
InlineKeyboardButton("📊 Status", callback_data="btn_status_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /conversations or /convs or /history_conv or /dialogs or /chats
@check_auth
async def conversations_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
text, markup = build_conversations_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /switchconv or /switch_conv or /openconv or /useconv or /selectconv
@check_auth
async def switch_conv_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
if not context.args:
text, markup = build_conversations_menu(chat_id)
msg_prompt = (
"💡 <i>لطفاً شناسه یا شماره گفتگو را مشخص کنید، یا از لیست زیر انتخاب نمایید:</i>\n\n"
"<b>مثال:</b> <code>/switchconv 1</code> یا <code>/switchconv 6c56254f</code>\n\n"
if is_fa else
"💡 <i>Please specify a conversation number or ID, or select from the list below:</i>\n\n"
"<b>Example:</b> <code>/switchconv 1</code> or <code>/switchconv 6c56254f</code>\n\n"
)
await update.message.reply_html(msg_prompt + text, reply_markup=markup)
return
target = context.args[0].strip()
success, msg_text, meta = session_manager.switch_conversation(chat_id, target)
if not success:
await update.message.reply_html(msg_text)
return
curr_proj = session_manager.get_current_project(chat_id)
target_cid = (curr_proj.conversation_id if curr_proj else target) or target
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
turns = meta.get("turns_count", 0)
if is_fa:
text = (
f"💬 <b>با موفقیت به گفتگو سوییچ کردید!</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'default')}</code>\n"
f"• 💬 <b>شناسه مکالمه فعال:</b> <code>{target_cid}</code>\n"
f"• 🔢 <b>تعداد نوبت‌ها:</b> <code>{turns} نوبت</code>\n"
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>پیام‌های بعدی شما در ادامه این گفتگو ارسال خواهند شد.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🗑️ حذف این گفتگو", callback_data=f"conv_del_ask_{target_cid}"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"💬 <b>Switched Conversation Successfully!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Active Conversation:</b> <code>{target_cid}</code>\n"
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>Your next messages will continue in this conversation.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🗑️ Delete this Chat", callback_data=f"conv_del_ask_{target_cid}"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /delconv or /deleteconv or /del_conv or /delete_conv
@check_auth
async def delete_conv_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
if not context.args:
if not curr_proj.conversation_id:
msg = (
"⚠️ گفتگوی فعالی برای حذف وجود ندارد.\n\n"
"💡 <i>برای حذف گفتگوی خاص:</i> <code>/delconv &lt;شماره یا شناسه&gt;</code>\n"
"لیست گفتگوها: <code>/conversations</code>"
if is_fa else
"⚠️ No active conversation to delete.\n\n"
"💡 <i>To delete a specific conversation:</i> <code>/delconv &lt;number or ID&gt;</code>\n"
"List: <code>/conversations</code>"
)
await update.message.reply_html(msg)
return
target_cid = curr_proj.conversation_id
if is_fa:
text = (
f"⚠️ <b>تأیید حذف گفتگوی فعال:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>شناسه گفتگو:</b> <code>{target_cid}</code>\n\n"
f"آیا مطمئن هستید که می‌خواهید این گفتگو را حذف کنید؟"
)
keyboard = [
[
InlineKeyboardButton("🗑️ بله، حذف شود", callback_data=f"conv_del_confirm_{target_cid}"),
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
]
]
else:
text = (
f"⚠️ <b>Confirm Delete Active Conversation:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Conversation ID:</b> <code>{target_cid}</code>\n\n"
f"Are you sure you want to delete this conversation?"
)
keyboard = [
[
InlineKeyboardButton("🗑️ Yes, Delete", callback_data=f"conv_del_confirm_{target_cid}"),
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
]
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
return
target = context.args[0].strip()
success, msg_text = session_manager.delete_conversation(chat_id, target)
if not success:
await update.message.reply_html(msg_text)
return
if is_fa:
text = (
f"{msg_text}\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>گفتگوی فعال فعلی:</b> <code>{curr_proj.conversation_id or '🟢 نشست تازه (آماده)'}</code>"
)
else:
text = (
f"{msg_text}\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Active Conversation:</b> <code>{curr_proj.conversation_id or '🟢 New Session (Ready)'}</code>"
)
keyboard = [
[
InlineKeyboardButton("📜 تاریخچه گفتگوها" if is_fa else "📜 Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
]
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /settopic or /settitle or /convtitle
@check_auth
async def set_conv_title_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
if not context.args:
msg = (
"💡 <b>راهنمای تنظیم عنوان گفتگو:</b>\n\n"
"<code>/settopic &lt;عنوان یا موضوع جدید&gt;</code>\n"
"یا برای گفتگوی خاص:\n"
"<code>/settopic &lt;شماره یا شناسه گفتگو&gt; &lt;عنوان جدید&gt;</code>"
if is_fa else
"💡 <b>Set Conversation Topic:</b>\n\n"
"<code>/settopic &lt;New Title&gt;</code>\n"
"Or for a specific conversation:\n"
"<code>/settopic &lt;number or ID&gt; &lt;New Title&gt;</code>"
)
await update.message.reply_html(msg)
return
first_arg = context.args[0].strip()
# Check if first arg is conv index or id
if (first_arg.isdigit() or first_arg.startswith("#") or len(first_arg) > 20) and len(context.args) > 1:
target_cid = first_arg
new_title = " ".join(context.args[1:]).strip()
else:
target_cid = curr_proj.conversation_id
new_title = " ".join(context.args).strip()
ok, msg, matched_cid = session_manager.set_conversation_title(chat_id, title=new_title, conv_id=target_cid)
if not ok:
await update.message.reply_html(msg)
return
keyboard = [
[
InlineKeyboardButton("📜 تاریخچه گفتگوها" if is_fa else "📜 Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
]
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /clearconvs or /deleteallconvs
@check_auth
async def clear_convs_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
if is_fa:
text = (
f"⚠️ <b>تأیید پاکسازی تمام گفتگوها:</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید تمام تاریخچه گفتگوهای پروژه <code>{escape_html(curr_proj.name)}</code> را پاکسازی و حذف نمایید؟\n\n"
f"<i>این عملیات تمام لاگ‌ها و مکالمات قبلی این پروژه را حذف خواهد کرد و قابل بازگشت نیست.</i>"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، حذف همه گفتگوها", callback_data="conv_clear_all_confirm"),
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
]
]
else:
text = (
f"⚠️ <b>Confirm Clear All Conversations:</b>\n\n"
f"Are you sure you want to permanently delete all conversation history for project <code>{escape_html(curr_proj.name)}</code>?\n\n"
f"<i>This action cannot be undone.</i>"
)
keyboard = [
[
InlineKeyboardButton("💥 Yes, Delete All", callback_data="conv_clear_all_confirm"),
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
]
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /git or /repo
@check_auth
async def git_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = await build_git_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
# Command: /publish
@check_auth
async def publish_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
pub_msg = " ".join(context.args).strip() if context.args else f"Manual release published via /publish"
status_msg = await update.message.reply_html("🚀 <i>در حال انتشار تغییرات شاخه dev به شاخه production...</i>" if is_fa else "🚀 <i>Publishing dev branch to production...</i>")
ok, res_msg, p_info = await git_manager.git_publish(curr_proj.workspace, message=pub_msg, repo_name=curr_proj.name)
urls = git_manager.get_repo_urls(curr_proj.name)
buttons = [
[
InlineKeyboardButton("🚀 دیپلوی روی FTP" if is_fa else "🚀 Deploy to FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"),
]
]
if ok:
out = (
f"🚀 <b>عملیات انتشار به پروداکشن (Publish to Production) با موفقیت انجام شد:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
) if is_fa else (
f"🚀 <b>Published to Production Successfully:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
)
else:
out = f"⚠️ <b>خطا در انتشار:</b>\n{res_msg}"
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True)
# Command: /ftp
@check_auth
async def ftp_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = await build_ftp_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
# Command: /ssh or /remotedev
@check_auth
async def ssh_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = await build_ssh_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
# Command: /sync
@check_auth
async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
status_msg = await update.message.reply_html("🔄 <i>در حال همگام‌سازی با مخزن گیت (Gitea)...</i>" if is_fa else "🔄 <i>Syncing with Gitea repository...</i>")
ok, res_msg = await git_manager.git_sync(curr_proj.workspace, message="Manual sync via /sync", repo_name=curr_proj.name)
urls = git_manager.get_repo_urls(curr_proj.name)
if is_fa:
out = (
f"🔄 <b>گزارش همگام‌سازی مخزن گیت (Git Sync):</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"📋 <b>جزییات:</b>\n{res_msg}"
)
else:
out = (
f"🔄 <b>Git Sync Report:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"📋 <b>Details:</b>\n{res_msg}"
)
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True)
# Command: /commit
@check_auth
async def commit_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
commit_msg = " ".join(context.args).strip() if context.args else f"Manual commit via /commit for {curr_proj.name}"
status_msg = await update.message.reply_html("💾 <i>در حال ثبت کامیت و ارسال به Gitea...</i>" if is_fa else "💾 <i>Committing and pushing to Gitea...</i>")
ok, res_msg = await git_manager.git_commit_and_push(curr_proj.workspace, message=commit_msg, repo_name=curr_proj.name)
urls = git_manager.get_repo_urls(curr_proj.name)
if is_fa:
out = (
f"💾 <b>گزارش ثبت کامیت و پوش:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"📝 {res_msg}"
)
else:
out = (
f"💾 <b>Commit & Push Report:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"📝 {res_msg}"
)
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True)
# Command: /undo or /revert
@check_auth
async def undo_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
commit_target = context.args[0].strip() if context.args else "HEAD"
status_msg = await update.message.reply_html("↩️ <i>در حال لغو تغییرات (Git Revert)...</i>" if is_fa else "↩️ <i>Reverting commit...</i>")
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_target)
urls = git_manager.get_repo_urls(curr_proj.name)
buttons = [
[
InlineKeyboardButton("📜 تاریخچه کامیت‌ها" if is_fa else "📜 Commit History", callback_data="git_hist:1"),
InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"),
]
]
if is_fa:
out = (
f"↩️ <b>گزارش لغو تغییرات (Git Undo / Revert):</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🎯 <b>کامیت هدف:</b> <code>{escape_html(commit_target)}</code>\n"
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
)
else:
out = (
f"↩️ <b>Git Undo / Revert Report:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🎯 <b>Target Commit:</b> <code>{escape_html(commit_target)}</code>\n"
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
)
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(buttons))
# Command: /model
@check_auth
async def model_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>No active project found!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
if context.args:
req_model = context.args[0].lower().strip()
matched = None
for m in AVAILABLE_MODELS.keys():
if req_model == m.lower():
matched = m
break
if not matched:
from bot_actions import resolve_model_name
matched = resolve_model_name(req_model)
if matched:
await session_manager.set_model(chat_id, matched)
msg = (
f"🧠 مدل پروژه <code>{escape_html(curr_proj.name)}</code> به <code>{matched}</code> تغییر یافت."
if is_fa else
f"🧠 Model for project <code>{escape_html(curr_proj.name)}</code> switched to <code>{matched}</code>."
)
await update.message.reply_html(msg)
return
keyboard = []
for model_id, label in AVAILABLE_MODELS.items():
is_selected = "✅ " if model_id == curr_proj.model else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_model:{model_id}")])
keyboard.append([InlineKeyboardButton("🔙 بازگشت به پروژه‌ها" if is_fa else "🔙 Back to Projects", callback_data="proj_menu")])
title = (
f"🧠 <b>انتخاب مدل هوش مصنوعی</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>مدل فعلی:</b> <code>{curr_proj.model}</code>"
if is_fa else
f"🧠 <b>Select AI Model</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>Current Model:</b> <code>{curr_proj.model}</code>"
)
await update.message.reply_html(title, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /lang or /language
@check_auth
async def lang_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if context.args:
req_lang = context.args[0].lower().strip()
req_lang = LANG_ALIASES.get(req_lang, req_lang)
if req_lang in AVAILABLE_LANGUAGES:
await session_manager.set_language(chat_id, req_lang)
display = get_lang_display(req_lang)
msg = f"🌐 زبان پاسخ‌دهی به <code>{escape_html(display)}</code> تغییر یافت." if is_fa else f"🌐 Response language changed to <code>{escape_html(display)}</code>."
await update.message.reply_html(msg)
return
keyboard = []
row = []
for code, label in AVAILABLE_LANGUAGES.items():
is_selected = "✅ " if (session.language.lower() == code.lower() or session.language == label) else ""
row.append(InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_lang:{code}"))
if len(row) == 2:
keyboard.append(row)
row = []
if row:
keyboard.append(row)
curr_display = get_lang_display(session.language)
title = f"🌐 <b>انتخاب زبان پاسخ‌دهی</b>\nزبان فعلی: <code>{escape_html(curr_display)}</code>" if is_fa else f"🌐 <b>Select Response Language</b>\nCurrent: <code>{escape_html(curr_display)}</code>"
await update.message.reply_html(title, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /effort
@check_auth
async def effort_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>No active project found!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
if context.args:
eff = context.args[0].lower().strip()
if eff in AVAILABLE_EFFORTS:
await session_manager.set_effort(chat_id, eff)
msg = f"⚡ سطح استدلال برای پروژه <code>{escape_html(curr_proj.name)}</code> روی <code>{eff}</code> تنظیم شد." if is_fa else f"⚡ Reasoning effort for project <code>{escape_html(curr_proj.name)}</code> set to <code>{eff}</code>."
await update.message.reply_html(msg)
return
keyboard = []
for e in AVAILABLE_EFFORTS:
is_selected = "✅ " if e == curr_proj.effort else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{e.capitalize()} Effort", callback_data=f"set_effort:{e}")])
title = (
f"⚡ <b>تنظیم سطح استدلال (Effort)</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>سطح فعلی:</b> <code>{curr_proj.effort}</code>"
if is_fa else
f"⚡ <b>Set Reasoning Effort</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>Current Effort:</b> <code>{curr_proj.effort}</code>"
)
await update.message.reply_html(title, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /workspace
@check_auth
async def workspace_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_admin_user = settings.is_admin(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>You have not created any projects yet!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
if context.args:
new_ws = " ".join(context.args).strip()
path = Path(new_ws).expanduser().resolve()
if not is_admin_user:
user_base = (Path("/root/projects") / str(chat_id)).resolve()
proj_base = Path(curr_proj.workspace).resolve()
try:
is_sub_user = path == user_base or user_base in path.parents
is_sub_proj = path == proj_base or proj_base in path.parents
if not (is_sub_user or is_sub_proj):
msg = (
"⛔ <b>خطای دسترسی:</b> تغییر مسیر فقط در محدوده دایرکتوری اختصاصی پروژه مجاز است."
if is_fa else
"⛔ <b>Access Denied:</b> Workspace path can only be set within your project directory."
)
await update.message.reply_html(msg)
return
except Exception:
msg = "⛔ مسیر نامعتبر است." if is_fa else "⛔ Invalid path."
await update.message.reply_html(msg)
return
os.makedirs(path, exist_ok=True)
await session_manager.set_workspace(chat_id, str(path))
msg = (
f"📂 دایرکتوری کاری پروژه <code>{escape_html(curr_proj.name)}</code> به <code>{escape_html(str(path))}</code> تغییر یافت."
if is_fa else
f"📂 Workspace for project <code>{escape_html(curr_proj.name)}</code> set to <code>{escape_html(str(path))}</code>."
)
await update.message.reply_html(msg)
return
if is_fa:
text = (
f"📂 <b>دایرکتوری کاری پروژه {escape_html(curr_proj.name)}:</b>\n"
f"<code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"<i>برای تغییر دایرکتوری این پروژه دستور زیر را ارسال کنید:</i>\n"
f"<code>/workspace /path/to/directory</code>"
)
else:
text = (
f"📂 <b>Workspace for project {escape_html(curr_proj.name)}:</b>\n"
f"<code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"<i>To change workspace for this project, send:</i>\n"
f"<code>/workspace /path/to/directory</code>"
)
await update.message.reply_html(text)
# Command: /status
@check_auth
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
accessible = session_manager.get_all_accessible_projects(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
if is_fa:
text = (
f"📊 <b>وضعیت سیستم و ربات AGY</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <i>هیچ پروژه‌ای ایجاد/انتخاب نشده است</i>\n"
f"• 💡 <b>ایجاد پروژه جدید:</b> <code>/newproject &lt;نام&gt;</code>\n"
f"• 🌐 <b>زبان پاسخ‌دهی:</b> <code>{get_lang_display(session.language)}</code>\n"
f"• 📁 <b>تعداد کل پروژه‌ها:</b> <code>0</code>"
)
keyboard = [
[InlineKeyboardButton("➕ ساخت اولین پروژه", callback_data="proj_new")],
[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")],
]
else:
text = (
f"📊 <b>System & AGY Bot Status</b>\n\n"
f"• 📁 <b>Active Project:</b> <i>None</i>\n"
f"• 💡 <b>Create Project:</b> <code>/newproject &lt;name&gt;</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(session.language)}</code>\n"
f"• 📁 <b>Total Projects:</b> <code>0</code>"
)
keyboard = [
[InlineKeyboardButton(" Create First Project", callback_data="proj_new")],
[InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard")],
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
return
try:
total, used, free = shutil.disk_usage(curr_proj.workspace)
disk_free_gb = f"{free / (1024**3):.1f} GB"
except Exception:
disk_free_gb = "N/A"
ctx_info = f"{curr_proj.last_context_length:,}" if curr_proj.last_context_length else "0"
tok_info = f"{curr_proj.last_total_tokens:,}" if curr_proj.last_total_tokens else "0"
owner_str = "(مالک: شما)" if curr_proj.owner_id == chat_id else f"(مالک: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
owner_str_en = "(Owner: You)" if curr_proj.owner_id == chat_id else f"(Owner: <code>{curr_proj.owner_id}</code>)" if curr_proj.owner_id else ""
if is_fa:
text = (
f"📊 <b>وضعیت سیستم و نشست کاربری</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str}\n"
f"• 📂 <b>دایرکتوری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🧠 <b>مدل هوش مصنوعی:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>زبان پاسخ‌دهی:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 💬 <b>شناسه مکالمه:</b> <code>{curr_proj.conversation_id or 'جدید'}</code>\n"
f"• 📏 <b>طول کانتکست:</b> <code>{ctx_info} توکن</code>\n"
f"• 🪙 <b>مجموع توکن‌ها:</b> <code>{tok_info}</code>\n"
f"• 💾 <b>فضای آزاد دیسک:</b> <code>{disk_free_gb}</code>\n"
f"• ⚙️ <b>وضعیت پردازش:</b> {'⏳ در حال اجرا' if session.turn_in_progress else '🟢 آماده'}\n"
f"• 📁 <b>تعداد کل پروژه‌ها:</b> <code>{len(accessible)}</code>"
)
keyboard = [
[
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
InlineKeyboardButton("📈 سهمیه مصرف AGY", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("🖥 سخت‌افزار سرور (RAM/CPU)", callback_data="btn_hw_menu"),
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"📊 <b>System & Session Status</b>\n\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str_en}\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🧠 <b>AI Model:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>Reasoning Effort:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 💬 <b>Conversation ID:</b> <code>{curr_proj.conversation_id or 'New'}</code>\n"
f"• 📏 <b>Context Length:</b> <code>{ctx_info} tokens</code>\n"
f"• 🪙 <b>Total Tokens:</b> <code>{tok_info}</code>\n"
f"• 💾 <b>Disk Free:</b> <code>{disk_free_gb}</code>\n"
f"• ⚙️ <b>Turn Status:</b> {'⏳ Running' if session.turn_in_progress else '🟢 Idle'}\n"
f"• 📁 <b>Total Accessible Projects:</b> <code>{len(accessible)}</code>"
)
keyboard = [
[
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("🖥 Server Hardware", callback_data="btn_hw_menu"),
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /server or /hardware or /ram or /cpu or /sysinfo
@check_auth
async def server_hardware_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = build_server_hardware_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /usage or /quota or /credits
@check_auth
async def usage_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
status_msg = await update.message.reply_html("⏳ <i>در حال استعلام سهمیه و مصرف AGY...</i>" if is_fa else "⏳ <i>Querying AGY quota & usage...</i>")
text, markup = await build_usage_report(chat_id)
await status_msg.edit_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
# Command: /account or /profile_auth or /acc
@check_auth
async def account_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = build_account_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /login or /signin
@check_auth
async def login_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if context.args:
# User provided auth code directly: /login 4/0A...
auth_code = " ".join(context.args).strip()
wait_msg = await update.message.reply_html("⏳ <i>در حال ثبت و بررسی کد احراز هویت گوگل...</i>" if is_fa else "⏳ <i>Submitting and verifying Google Auth code...</i>")
success, msg = await auth_manager.complete_oauth_login(chat_id, auth_code)
if success:
text, markup = build_account_menu(chat_id)
await wait_msg.edit_text(f"{msg}\n\n{text}", parse_mode=constants.ParseMode.HTML, reply_markup=markup)
else:
await wait_msg.edit_text(msg, parse_mode=constants.ParseMode.HTML)
return
wait_msg = await update.message.reply_html("⏳ <i>در حال ایجاد نشست و لینک ورود اختصاصی گوگل...</i>" if is_fa else "⏳ <i>Generating Google OAuth login URL...</i>")
success, msg, auth_url = await auth_manager.start_oauth_login(chat_id)
if not success or not auth_url:
await wait_msg.edit_text(f"❌ {msg}", parse_mode=constants.ParseMode.HTML)
return
if is_fa:
prompt_text = (
"🔑 <b>ورود به حساب اختصاصی گوگل (AGY OAuth Login)</b>\n\n"
"۱. روی دکمه زیر کلیک کنید تا صفحه رسمی ورود به حساب گوگل باز شود.\n"
"۲. وارد حساب کاربری گوگل خود شوید و اجازه دسترسی را تایید نمایید.\n"
"۳. <b>کد تایید احراز هویت (Authorization Code)</b> نمایش داده شده را کپی کرده و در پاسخ به همین پیام برای ربات ارسال کنید (یا دستور <code>/login &lt;کد&gt;</code> را بفرستید).\n\n"
"⏳ <i>فرصت ارسال کد: ۶۰ ثانیه</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 ورود به حساب کاربری گوگل", url=auth_url)],
[InlineKeyboardButton("❌ انصراف / لغو", callback_data="btn_account_cancel_login")],
]
else:
prompt_text = (
"🔑 <b>Google Account Login (AGY OAuth)</b>\n\n"
"1. Click the button below to open Google's authorization page.\n"
"2. Sign in to your Google Account and grant access permissions.\n"
"3. Copy the <b>Authorization Code</b> and paste it as a message to this bot.\n\n"
"⏳ <i>Timeout: 60 seconds</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 Sign in with Google", url=auth_url)],
[InlineKeyboardButton("❌ Cancel", callback_data="btn_account_cancel_login")],
]
await wait_msg.edit_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
# Command: /logout or /signout
@check_auth
async def logout_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
success, msg = auth_manager.logout_user(chat_id)
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{'✅' if success else '❌'} {msg}\n\n{text}", reply_markup=markup)
# Command: /memory or /memories
@check_auth
async def memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
from memory_manager import memory_manager
if not context.args:
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
await update.message.reply_html(text, reply_markup=markup)
return
subcmd = context.args[0].lower().strip()
if subcmd in ("project", "proj", "p"):
text, markup = build_memory_menu(chat_id, view_type="project", page=0)
await update.message.reply_html(text, reply_markup=markup)
return
if subcmd in ("user", "u", "me", "profile", "private"):
text, markup = build_memory_menu(chat_id, view_type="user", page=0)
await update.message.reply_html(text, reply_markup=markup)
return
if subcmd in ("global", "g", "public", "system"):
if not is_admin_user:
await update.message.reply_html(
"🚫 <b>خطای دسترسی: مشاهده و مدیریت حافظه عمومی سیستم فقط مختص مدیر است.</b>"
if is_fa
else "🚫 <b>Access Denied: Viewing global system memory is restricted to administrators.</b>"
)
return
text, markup = build_memory_menu(chat_id, view_type="global", page=0)
await update.message.reply_html(text, reply_markup=markup)
return
# Add memory manually: /memory add <global|user|project> <key> | <content>
if subcmd in ("add", "set", "save") and len(context.args) > 2:
target_type = context.args[1].lower().strip()
if target_type not in ("global", "user", "project"):
target_type = "user"
rest = " ".join(context.args[1:]).strip()
else:
rest = " ".join(context.args[2:]).strip()
if target_type == "global" and not is_admin_user:
await update.message.reply_html("🚫 <b>خطای دسترسی: فقط مدیر سیستم می‌تواند خاطره عمومی ثبت کند.</b>" if is_fa else "🚫 <b>Access Denied.</b>")
return
if "|" in rest:
key_part, content_part = rest.split("|", 1)
else:
parts = rest.split(None, 1)
key_part = parts[0]
content_part = parts[1] if len(parts) > 1 else ""
key_clean = key_part.strip().lower()
content_clean = content_part.strip()
if not key_clean or not content_clean:
await update.message.reply_html(
"💡 <i>راهنمای افزودن دستی خاطره:</i>\n<code>/memory add [global|user|project] &lt;key&gt; | &lt;محتوا&gt;</code>\n\n<b>مثال:</b>\n<code>/memory add global default_php | همیشه از PHP مدرن استفاده شود</code>"
if is_fa
else "💡 <i>Usage:</i> <code>/memory add [global|user|project] &lt;key&gt; | &lt;content&gt;</code>"
)
return
p_name = curr_proj.name if (curr_proj and target_type == "project") else "default"
item, is_created = memory_manager.save_or_update(
type_=target_type,
key=key_clean,
content=content_clean,
user_id=chat_id,
project_name=p_name if target_type == "project" else None,
category="rule" if target_type == "global" else "preference",
importance=5 if target_type == "global" else 3,
created_by=chat_id,
)
msg = f"✅ <b>خاطره با موفقیت ثبت شد:</b>\n• 🏷️ بخش: {item.type}\n• 🔑 کلید: <code>{escape_html(item.key)}</code>\n• 📝 محتوا: {escape_html(item.content)}" if is_fa else f"✅ <b>Memory saved:</b> [{item.type}] {item.key}"
text, markup = build_memory_menu(chat_id, view_type=target_type, page=0)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
if subcmd in ("clear", "clean", "purge"):
target_type = context.args[1].lower() if len(context.args) > 1 else "project"
if target_type not in ("global", "user", "project"):
target_type = "project" if curr_proj else "user"
p_name = curr_proj.name if curr_proj else "default"
try:
cnt = memory_manager.clear_memories(
type_=target_type, user_id=chat_id, project_name=p_name, is_admin=is_admin_user
)
type_label = f"پروژه {p_name}" if target_type == "project" else ("عمومی" if target_type == "global" else "شخصی کاربر")
msg = f"🧹 <b>تعداد {cnt} خاطره {type_label} با موفقیت پاک‌سازی شد.</b>" if is_fa else f"🧹 <b>{cnt} {target_type} memories cleared.</b>"
except PermissionError:
msg = "🚫 <b>خطای دسترسی: فقط ادمین مجاز به پاک‌سازی خاطرات عمومی است.</b>" if is_fa else "🚫 <b>Access denied.</b>"
await update.message.reply_html(msg)
return
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
await update.message.reply_html(text, reply_markup=markup)
# Command: /context or /tokens
@check_auth
async def context_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
stats_usage = {
"input_tokens": curr_proj.last_context_length or 0,
"total_tokens": curr_proj.last_total_tokens or curr_proj.last_context_length or 0,
}
stats_html = format_context_stats(
usage=stats_usage if curr_proj.last_context_length else None,
duration=0.0,
model=curr_proj.model,
effort=curr_proj.effort,
lang=session.language,
project_name=curr_proj.name,
conversation_id=curr_proj.conversation_id,
)
if is_fa:
text = (
f"📏 <b>وضعیت کانتکست و مصرف توکن‌ها:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>طول کانتکست فعلی:</b> <code>{curr_proj.last_context_length or 0:,} توکن</code>\n"
f"• 📊 <b>کل توکن‌های مصرفی:</b> <code>{curr_proj.last_total_tokens or 0:,} توکن</code>\n\n"
f"🏷️ <b>نشانگر خلاصه:</b>\n{stats_html}"
)
else:
text = (
f"📏 <b>Context & Token Statistics:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>Current Context Length:</b> <code>{curr_proj.last_context_length or 0:,} tokens</code>\n"
f"• 📊 <b>Total Tokens Used:</b> <code>{curr_proj.last_total_tokens or 0:,} tokens</code>\n\n"
f"🏷️ <b>Badge Summary:</b>\n{stats_html}"
)
await update.message.reply_html(text, reply_markup=get_usage_buttons(session.language))
# Command: /cancel
@check_auth
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
cancelled = session_manager.cancel_active_task(chat_id)
session.turn_in_progress = False
session_manager.save()
if cancelled:
msg = "🛑 <b>عملیات و پردازش فعلی لغو شد.</b>" if is_fa else "🛑 <b>Active process / task was cancelled.</b>"
else:
msg = "️ <i>هیچ پردازش فعالی در حال اجرا نبود.</i>" if is_fa else "️ <i>No active task is currently running.</i>"
await update.message.reply_html(msg)
# Command: /tasks or /schedule
@check_auth
async def tasks_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
curr_proj = session_manager.get_current_project(chat_id)
if not context.args:
text, markup = build_tasks_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
subcmd = context.args[0].lower().strip()
raw_args = " ".join(context.args).strip()
if subcmd == "list":
text, markup = build_tasks_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
if subcmd in ("help", "guide"):
text, markup = build_task_add_guide(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
if subcmd in ("clear", "clean", "purge"):
cnt = task_scheduler.clear_completed_tasks(chat_id)
msg = f"🧹 <b>{cnt} تسک پایان‌یافته پاکسازی شد.</b>" if is_fa else f"🧹 <b>{cnt} completed tasks cleared.</b>"
text, markup = build_tasks_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
if subcmd in ("run", "exec", "start") and len(context.args) > 1:
task_id = context.args[1].strip()
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
return
status_msg = await update.message.reply_html(f"⏳ <b>در حال اجرای تسک <code>{task_id}</code>...</b>" if is_fa else f"⏳ <b>Executing task <code>{task_id}</code>...</b>")
success, out = await task_scheduler.execute_task_now(task_id, context.application)
try:
await status_msg.delete()
except Exception:
pass
return
if subcmd in ("pause", "stop") and len(context.args) > 1:
task_id = context.args[1].strip()
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
return
ok = task_scheduler.pause_task(task_id)
if ok:
text, markup = build_task_detail_menu(chat_id, task_id)
await update.message.reply_html(f"⏸️ <b>تسک <code>{task_id}</code> متوقف شد.</b>\n\n{text}", reply_markup=markup)
else:
await update.message.reply_html("❌ <b>تسک یافت نشد یا از قبل متوقف است.</b>" if is_fa else "❌ <b>Task not found or already paused.</b>")
return
if subcmd in ("resume", "unpause", "play") and len(context.args) > 1:
task_id = context.args[1].strip()
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
return
ok = task_scheduler.resume_task(task_id)
if ok:
text, markup = build_task_detail_menu(chat_id, task_id)
await update.message.reply_html(f"▶️ <b>تسک <code>{task_id}</code> فعال شد.</b>\n\n{text}", reply_markup=markup)
else:
await update.message.reply_html("❌ <b>تسک یافت نشد یا در حال حاضر فعال است.</b>" if is_fa else "❌ <b>Task not found or already active.</b>")
return
if subcmd in ("del", "delete", "remove", "rm") and len(context.args) > 1:
task_id = context.args[1].strip()
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
return
ok = task_scheduler.delete_task(task_id)
if ok:
text, markup = build_tasks_menu(chat_id)
await update.message.reply_html(f"🗑️ <b>تسک <code>{task_id}</code> با موفقیت حذف شد.</b>\n\n{text}", reply_markup=markup)
else:
await update.message.reply_html("❌ <b>تسک یافت نشد.</b>" if is_fa else "❌ <b>Task not found.</b>")
return
if subcmd in ("info", "view", "show") and len(context.args) > 1:
task_id = context.args[1].strip()
text, markup = build_task_detail_menu(chat_id, task_id)
await update.message.reply_html(text, reply_markup=markup)
return
# Add task parsing: /schedule add <timing> | <content> OR /schedule <timing> | <content>
add_payload = raw_args
if add_payload.lower().startswith("add "):
add_payload = add_payload[4:].strip()
if "|" not in add_payload:
text, markup = build_task_add_guide(chat_id)
await update.message.reply_html(
f"⚠️ <b>فرمت دستور صحیح نیست!</b>\n\n"
f"لطفاً از علامت خط عمودی <code>|</code> برای جدا کردن زمان‌بندی و متن پرامپت استفاده کنید:\n"
f"<code>/schedule &lt;زمان‌بندی [تعداد تکرار]&gt; | &lt;پرامپت یا دستور&gt;</code>\n\n"
f"{text}",
reply_markup=markup
)
return
timing_part, content_part = [p.strip() for p in add_payload.split("|", 1)]
if not timing_part or not content_part:
await update.message.reply_html("⚠️ زمان‌بندی یا متن دستور خالی است." if is_fa else "⚠️ Timing or content is empty.")
return
if not curr_proj:
await update.message.reply_html("⚠️ لطفاً ابتدا با <code>/newproject</code> یک پروژه بسازید." if is_fa else "⚠️ Please create a project first using <code>/newproject</code>.")
return
# Task type detection
task_type = "prompt"
effective_content = content_part
if content_part.lower().startswith(("cmd:", "bash:", "sh:", "exec:")):
task_type = "command"
effective_content = content_part.split(":", 1)[1].strip()
elif content_part.lower().startswith(("remind:", "reminder:", "msg:", "یادآوری:", "پیام:")):
task_type = "reminder"
effective_content = content_part.split(":", 1)[1].strip()
try:
task = task_scheduler.add_task(
chat_id=chat_id,
creator_id=chat_id,
project_name=curr_proj.name,
task_type=task_type,
content=effective_content,
timing_spec=timing_part,
)
except Exception as parse_err:
await update.message.reply_html(f"❌ <b>خطا در زمان‌بندی:</b>\n{str(parse_err)}")
return
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 "نامحدود (دائمی)"
if is_fa:
resp_text = (
f"🎉 <b>تسک زمان‌بندی شده با موفقیت ثبت و فعال شد!</b>\n\n"
f"• 🏷️ <b>شناسه تسک:</b> <code>{task.id}</code>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</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[:100])}</code>\n\n"
f"💡 نتیجه اجرا به صورت خودکار در زمان مشخص شده برای شما ارسال خواهد شد."
)
else:
resp_text = (
f"🎉 <b>Scheduled task created and activated!</b>\n\n"
f"• 🏷️ <b>Task ID:</b> <code>{task.id}</code>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</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> {task_type}\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[:100])}</code>\n\n"
f"💡 Results will be automatically delivered here upon execution."
)
keyboard = [
[
InlineKeyboardButton("▶️ اجرای فوری هم‌اکنون" if is_fa else "▶️ Run Now", callback_data=f"task_run:{task.id}"),
InlineKeyboardButton("⚙️ مدیریت این تسک" if is_fa else "⚙️ Task Details", callback_data=f"task_detail:{task.id}"),
],
[
InlineKeyboardButton("⏰ لیست همه تسک‌ها" if is_fa else "⏰ All Tasks", callback_data="btn_tasks_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
]
]
await update.message.reply_html(resp_text, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /invite [user_id] or /invite link [uses] [hours]
@check_auth
async def invite_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
user_id = update.effective_user.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not settings.is_admin(user_id):
msg = "⛔ <b>این دستور مخصوص مدیران سیستم است.</b>" if is_fa else "⛔ <b>This command is restricted to administrators.</b>"
await update.message.reply_html(msg)
return
bot_username = (await context.application.bot.get_me()).username or "AGYBot"
if not context.args:
text, markup = build_users_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
sub = context.args[0].lower().strip()
if sub in ("link", "gen", "newlink", "token"):
max_uses = 1
hours = None
if len(context.args) > 1 and context.args[1].isdigit():
max_uses = int(context.args[1])
if len(context.args) > 2 and context.args[2].isdigit():
hours = float(context.args[2])
token = invite_manager.create_invite(creator_id=user_id, max_uses=max_uses, duration_hours=hours)
link = f"https://t.me/{bot_username}?start={token.code}"
cap_str = f"{max_uses} کاربر" if max_uses > 0 else "نامحدود"
exp_str = f"{hours} ساعت" if hours else "بدون انقضا (دائمی)"
if is_fa:
msg = (
f"🔗 <b>لینک دعوت جدید اختصاصی ساخته شد!</b>\n\n"
f"• 📋 <b>لینک دعوت:</b>\n<code>{link}</code>\n\n"
f"• 🔑 <b>کد دعوت:</b> <code>{token.code}</code>\n"
f"• 👥 <b>ظرفیت استفاده:</b> <code>{cap_str}</code>\n"
f"• ⏳ <b>مدت اعتبار:</b> <code>{exp_str}</code>\n\n"
f"💡 <i>کاربر با کلیک روی این لینک به صورت خودکار به ربات دسترسی خواهد یافت.</i>"
)
else:
msg = (
f"🔗 <b>New Invite Link Generated!</b>\n\n"
f"• 📋 <b>Invite Link:</b>\n<code>{link}</code>\n\n"
f"• 🔑 <b>Code:</b> <code>{token.code}</code>\n"
f"• 👥 <b>Usage Limit:</b> <code>{max_uses}</code>\n"
f"• ⏳ <b>Valid For:</b> <code>{exp_str}</code>\n\n"
f"💡 <i>Anyone who clicks this link will be automatically authorized.</i>"
)
keyboard = [
[InlineKeyboardButton("📋 مشاهده همه لینک‌ها" if is_fa else "📋 All Links", callback_data="btn_invites_menu")],
[InlineKeyboardButton("👥 مدیریت کاربران" if is_fa else "👥 User Manager", callback_data="btn_users_menu")],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
target_str = sub
if not target_str.isdigit():
await update.message.reply_html(
"⚠️ <b>فرمت نامعتبر است!</b>\n\n"
"• دعوت با شناسه: <code>/invite &lt;user_id&gt;</code>\n"
"• ساخت لینک دعوت: <code>/invitelink</code> یا <code>/invite link</code>"
if is_fa else
"⚠️ <b>Invalid format!</b>\n\n"
"• Invite by ID: <code>/invite &lt;user_id&gt;</code>\n"
"• Create link: <code>/invitelink</code> or <code>/invite link</code>"
)
return
target_uid = int(target_str)
if settings.is_user_authorized(target_uid):
msg = f"️ کاربر <code>{target_uid}</code> از قبل در لیست مجاز قرار دارد." if is_fa else f"️ User <code>{target_uid}</code> is already authorized."
await update.message.reply_html(msg)
return
settings.add_authorized_user(target_uid)
if target_uid in invite_manager.pending_requests:
del invite_manager.pending_requests[target_uid]
invite_manager.save()
try:
user_welcome = (
f"🎉 <b>شما به ربات دستیار هوشمند Antigravity (AGY) دعوت شدید!</b>\n\n"
f"دسترسی شما توسط مدیر فعال شد. برای شروع /start را ارسال کنید."
if is_fa else
f"🎉 <b>You have been invited to Antigravity (AGY) Bot!</b>\n\n"
f"Access granted by administrator. Send /start to begin."
)
await context.application.bot.send_message(
chat_id=target_uid,
text=user_welcome,
parse_mode=constants.ParseMode.HTML,
)
except Exception:
pass
if is_fa:
msg = (
f"✅ <b>کاربر با موفقیت دعوت و مجاز شد!</b>\n\n"
f"• 🆔 <b>شناسه کاربر:</b> <code>{target_uid}</code>\n"
f"• 👥 <b>تعداد کل کاربران مجاز:</b> <code>{len(settings.allowed_user_ids)}</code>"
)
else:
msg = (
f"✅ <b>User authorized successfully!</b>\n\n"
f"• 🆔 <b>User ID:</b> <code>{target_uid}</code>\n"
f"• 👥 <b>Total Authorized:</b> <code>{len(settings.allowed_user_ids)}</code>"
)
keyboard = [
[InlineKeyboardButton("👥 لیست کاربران" if is_fa else "👥 Users List", callback_data="btn_users_menu")],
[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
# Command: /uninvite <user_id> or /revoke <user_id> or /ban <user_id>
@check_auth
async def uninvite_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
user_id = update.effective_user.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not settings.is_admin(user_id):
msg = "⛔ <b>این دستور مخصوص مدیران سیستم است.</b>" if is_fa else "⛔ <b>This command is restricted to administrators.</b>"
await update.message.reply_html(msg)
return
if not context.args or not context.args[0].isdigit():
msg = "راهنما: <code>/uninvite &lt;user_id&gt;</code>" if is_fa else "Usage: <code>/uninvite &lt;user_id&gt;</code>"
await update.message.reply_html(msg)
return
target_uid = int(context.args[0])
if target_uid in settings.admin_user_ids:
msg = "❌ <b>امکان لغو دسترسی ادمین وجود ندارد.</b>" if is_fa else "❌ <b>Cannot revoke access for an Administrator.</b>"
await update.message.reply_html(msg)
return
ok = settings.remove_authorized_user(target_uid)
if ok:
session_manager.cancel_active_task(target_uid)
msg = f"🚫 <b>دسترسی کاربر <code>{target_uid}</code> با موفقیت لغو شد.</b>" if is_fa else f"🚫 <b>Access revoked for user <code>{target_uid}</code>.</b>"
else:
msg = f"️ کاربر <code>{target_uid}</code> در لیست مجاز یافت نشد." if is_fa else f"️ User <code>{target_uid}</code> not found in whitelist."
text, markup = build_users_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
# Command: /users or /whitelist
@check_auth
async def users_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
user_id = update.effective_user.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not settings.is_admin(user_id):
msg = "⛔ <b>این پنل مخصوص مدیران سیستم است.</b>" if is_fa else "⛔ <b>This panel is restricted to administrators.</b>"
await update.message.reply_html(msg)
return
text, markup = build_users_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Helper to execute and send project backup
async def handle_project_backup_flow(
application,
chat_id: int,
proj_name: str,
workspace: str,
is_fa: bool,
status_msg=None,
):
try:
if status_msg:
init_text = (
f"⏳ <b>در حال فشرده‌سازی و ایجاد فایل‌های پشتیبان (Zip) از پروژه <code>{escape_html(proj_name)}</code>...</b>\n\n"
f"<i>لطفاً چند لحظه صبر کنید...</i>"
if is_fa else
f"⏳ <b>Compressing and generating zip backup files for project <code>{escape_html(proj_name)}</code>...</b>\n\n"
f"<i>Please wait...</i>"
)
try:
await status_msg.edit_text(init_text, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
chunks, file_count, total_size_mb, session_id = backup_manager.generate_backup(proj_name, workspace)
total_chunks = len(chunks)
if total_chunks == 0 or file_count == 0:
err_text = (
f"⚠️ <b>پوشه پروژه <code>{escape_html(proj_name)}</code> خالی است یا فایلی برای بکاپ یافت نشد.</b>"
if is_fa else
f"⚠️ <b>Project folder <code>{escape_html(proj_name)}</code> is empty or contains no files.</b>"
)
if status_msg:
await status_msg.edit_text(err_text, parse_mode=constants.ParseMode.HTML)
else:
await application.bot.send_message(chat_id=chat_id, text=err_text, parse_mode=constants.ParseMode.HTML)
backup_manager.cleanup(session_id)
return
date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Send each chunk sequentially
for idx, chunk_path in enumerate(chunks, start=1):
chunk_size_mb = chunk_path.stat().st_size / (1024 * 1024)
if total_chunks == 1:
caption = (
f"📦 <b>بکاپ کامل پروژه:</b> <code>{escape_html(proj_name)}</code>\n"
f"📅 <b>تاریخ:</b> <code>{date_str}</code>\n"
f"📊 <b>حجم فایل:</b> <code>{total_size_mb:.2f} MB</code>\n"
f"📁 <b>تعداد فایل‌ها:</b> <code>{file_count}</code>\n\n"
f"⚡ <i>تولید شده توسط Antigravity (AGY)</i>"
if is_fa else
f"📦 <b>Full Project Backup:</b> <code>{escape_html(proj_name)}</code>\n"
f"📅 <b>Date:</b> <code>{date_str}</code>\n"
f"📊 <b>File Size:</b> <code>{total_size_mb:.2f} MB</code>\n"
f"📁 <b>Total Files:</b> <code>{file_count}</code>\n\n"
f"⚡ <i>Generated by Antigravity (AGY)</i>"
)
else:
caption = (
f"📦 <b>بکاپ چندبخشی پروژه:</b> <code>{escape_html(proj_name)}</code> (بخش {idx} از {total_chunks})\n"
f"📄 <b>نام پارت:</b> <code>{escape_html(chunk_path.name)}</code>\n"
f"📊 <b>حجم این پارت:</b> <code>{chunk_size_mb:.2f} MB</code> (مجموع: <code>{total_size_mb:.2f} MB</code>)\n"
f"📁 <b>تعداد کل فایل‌ها:</b> <code>{file_count}</code>\n\n"
f"💡 <b>راهنمای استخراج پارت‌ها:</b>\n"
f"• لینوکس/مک: <code>cat *.zip.* &gt; project.zip</code>\n"
f"• ویندوز: کلیک‌راست روی پارت 001 و Extract با 7-Zip یا WinRAR"
if is_fa else
f"📦 <b>Multi-Part Backup:</b> <code>{escape_html(proj_name)}</code> (Part {idx}/{total_chunks})\n"
f"📄 <b>Part File:</b> <code>{escape_html(chunk_path.name)}</code>\n"
f"📊 <b>Part Size:</b> <code>{chunk_size_mb:.2f} MB</code> (Total: <code>{total_size_mb:.2f} MB</code>)\n"
f"📁 <b>Total Files:</b> <code>{file_count}</code>\n\n"
f"💡 <b>Extraction Guide:</b>\n"
f"• Linux/Mac: <code>cat *.zip.* &gt; project.zip</code>\n"
f"• Windows: Right click part .001 and Extract with 7-Zip or WinRAR"
)
with open(chunk_path, "rb") as doc_file:
await application.bot.send_document(
chat_id=chat_id,
document=doc_file,
filename=chunk_path.name,
caption=caption,
parse_mode=constants.ParseMode.HTML,
read_timeout=120,
write_timeout=120,
connect_timeout=60,
)
# Cleanup temp directory
backup_manager.cleanup(session_id)
done_msg = (
f"✅ <b>تهیه و ارسال بکاپ پروژه <code>{escape_html(proj_name)}</code> با موفقیت تکمیل شد!</b>\n\n"
f"• 📊 <b>تعداد فایل‌های ارسال شده:</b> <code>{total_chunks} پارت</code> (مجموع: <code>{total_size_mb:.2f} MB</code>)\n"
f"• 📁 <b>تعداد کدهای پروژه:</b> <code>{file_count} فایل</code>"
if is_fa else
f"✅ <b>Backup of project <code>{escape_html(proj_name)}</code> completed and sent successfully!</b>\n\n"
f"• 📊 <b>Files Sent:</b> <code>{total_chunks} part(s)</code> (Total: <code>{total_size_mb:.2f} MB</code>)\n"
f"• 📁 <b>Source Files:</b> <code>{file_count}</code>"
)
keyboard = [
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
]
]
if status_msg:
try:
await status_msg.edit_text(done_msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await application.bot.send_message(chat_id=chat_id, text=done_msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
else:
await application.bot.send_message(chat_id=chat_id, text=done_msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception as e:
logger.error(f"Backup failed for project {proj_name}: {e}", exc_info=True)
err_msg = (
f"❌ <b>خطا در تهیه فایل بکاپ:</b>\n<code>{escape_html(str(e))}</code>"
if is_fa else
f"❌ <b>Failed to create backup:</b>\n<code>{escape_html(str(e))}</code>"
)
if status_msg:
try:
await status_msg.edit_text(err_msg, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
await application.bot.send_message(chat_id=chat_id, text=err_msg, parse_mode=constants.ParseMode.HTML)
# Command: /backup [project_name] or /zip or /export
@check_auth
async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
target_name = None
if context.args:
target_name = context.args[0].strip()
accessible = session_manager.get_all_accessible_projects(chat_id)
if not accessible:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>"
if is_fa else
"⚠️ <b>You have not created any projects yet!</b>"
)
await update.message.reply_html(msg)
return
if target_name:
proj = accessible.get(target_name)
if not proj:
for k, p in accessible.items():
if p.name == target_name:
proj = p
break
if not proj:
msg = (
f"❌ پروژه با نام <code>{escape_html(target_name)}</code> یافت نشد."
if is_fa else
f"❌ Project <code>{escape_html(target_name)}</code> was not found."
)
await update.message.reply_html(msg)
return
else:
proj = session_manager.get_current_project(chat_id)
if not proj:
proj = list(accessible.values())[0]
status_msg = await update.message.reply_html(
f"⏳ <i>در حال آماده‌سازی و فشرده‌سازی بکاپ پروژه <code>{escape_html(proj.name)}</code>...</i>"
if is_fa else
f"⏳ <i>Preparing and compressing backup for project <code>{escape_html(proj.name)}</code>...</i>"
)
await handle_project_backup_flow(
application=context.application,
chat_id=chat_id,
proj_name=proj.name,
workspace=proj.workspace,
is_fa=is_fa,
status_msg=status_msg,
)
# Command: /invitelink
@check_auth
async def invitelink_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.args = ["link"] + (context.args or [])
await invite_command(update, context)
# Command: /auth <password>
async def auth_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.effective_user:
return
user_id = update.effective_user.id
if settings.is_user_authorized(user_id):
await update.message.reply_html("✅ <b>You are already authorized!</b>")
return
if not settings.auth_password:
await update.message.reply_html("⛔ Authentication via password is not configured.")
return
if not context.args:
await update.message.reply_html("Usage: <code>/auth &lt;password&gt;</code>")
return
password = context.args[0].strip()
if password == settings.auth_password:
settings.add_authorized_user(user_id)
await update.message.reply_html("🎉 <b>Authentication successful!</b> You now have access to the bot. Send /start to begin.")
else:
await update.message.reply_html("❌ <b>Incorrect password.</b> Access denied.")
# Callback Query Handler
async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
user_id = update.effective_user.id
if not settings.is_user_authorized(user_id):
try:
await query.answer("⛔ دسترسی محدود است / Unauthorized", show_alert=True)
except Exception:
pass
return
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id) or settings.is_admin(user_id)
data = query.data
if data == "btn_dashboard":
text, markup = build_main_dashboard(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_git_menu":
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_publish":
await query.answer("🚀 در حال انتقال تغییرات از dev به production..." if is_fa else "🚀 Publishing to production...")
if curr_proj:
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish dev to production via bot button", repo_name=curr_proj.name)
try:
await query.message.reply_html(res_msg, disable_web_page_preview=True)
except Exception:
pass
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_menu":
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("ftp_set:"):
field_name = data.split(":")[1]
if not curr_proj:
await query.answer("⚠️ پروژه‌ای یافت نشد." if is_fa else "⚠️ No project found.", show_alert=True)
return
USER_INPUT_STATES[chat_id] = {
"type": "ftp_set",
"field": field_name,
"project": curr_proj.name,
}
field_prompts = {
"host": (
"🌐 <b>لطفاً آدرس هاست یا سرور FTP را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>ftp.mysite.com</code> یا <code>185.120.30.40:21</code>"
) if is_fa else (
"🌐 <b>Please enter the FTP Host address:</b>\n\n"
"<i>Example:</i> <code>ftp.mysite.com</code> or <code>185.120.30.40:21</code>"
),
"user": (
"👤 <b>لطفاً نام کاربری (Username) اتصال FTP را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>deployer@mysite.com</code> یا <code>ftpuser</code>"
) if is_fa else (
"👤 <b>Please enter the FTP Username:</b>\n\n"
"<i>Example:</i> <code>deployer@mysite.com</code> or <code>ftpuser</code>"
),
"pass": (
"🔑 <b>لطفاً رمز عبور (Password) اتصال FTP را ارسال کنید:</b>\n\n"
"<i>(پیام پس از ثبت، جهت امنیت حذف یا پوشانده می‌شود)</i>"
) if is_fa else (
"🔑 <b>Please enter the FTP Password:</b>\n\n"
"<i>(Message will be stored securely)</i>"
),
"path": (
"📂 <b>لطفاً مسیر پوشه ریموت مقصد روی هاست را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>/public_html</code> یا <code>/domains/site.com/public_html</code>"
) if is_fa else (
"📂 <b>Please enter the remote target path:</b>\n\n"
"<i>Example:</i> <code>/public_html</code>"
),
}
prompt_text = field_prompts.get(field_name, "لطفاً مقدار جدید را ارسال کنید:")
cancel_kb = InlineKeyboardMarkup([[InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu")]])
await query.edit_message_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=cancel_kb)
elif data == "ftp_toggle_tls":
if curr_proj:
curr_proj.ftp_tls = not curr_proj.ftp_tls
session_manager.save()
status_str = "روشن (FTPS/TLS) 🔒" if curr_proj.ftp_tls else "خاموش (FTP معمولی) 🔓"
await query.answer(f"✅ پروتکل امن: {status_str}")
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "ftp_clear_conf":
confirm_text = (
"🗑️ <b>آیا از پاک‌سازی کامل اطلاعات FTP این پروژه اطمینان دارید؟</b>\n\n"
"تمام مشخصات سرور، کاربر و رمز عبور حذف خواهند شد."
) if is_fa else (
"🗑️ <b>Are you sure you want to clear FTP settings for this project?</b>"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، پاک شود" if is_fa else "💥 Yes, Clear", callback_data="ftp_clear_do"),
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data="btn_ftp_menu"),
]
]
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "ftp_clear_do":
if curr_proj:
curr_proj.ftp_host = None
curr_proj.ftp_port = 21
curr_proj.ftp_user = None
curr_proj.ftp_password = None
curr_proj.ftp_path = "/"
curr_proj.ftp_tls = False
session_manager.save()
await query.answer("🗑️ اطلاعات FTP با موفقیت پاک شد." if is_fa else "🗑️ FTP settings cleared.")
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_test":
await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...")
if curr_proj and curr_proj.ftp_host:
test_ok, test_msg = await ftp_manager.test_connection(
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user or "",
password=curr_proj.ftp_password or "",
path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
)
icon = "✅" if test_ok else "❌"
res_card = (
f"{icon} <b>نتیجه تست اتصال FTP:</b>\n\n"
f"• 🌐 <b>هاست:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 👤 <b>نام کاربری:</b> <code>{escape_html(curr_proj.ftp_user or '(بدون نام کاربری)')}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 🔒 <b>پروتکل:</b> {'FTPS/TLS امن 🔒' if curr_proj.ftp_tls else 'FTP معمولی'}\n\n"
f"📋 <b>گزارش ارتباط:</b>\n{test_msg}"
) if is_fa else (
f"{icon} <b>FTP Connection Test Result:</b>\n\n"
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 👤 <b>User:</b> <code>{escape_html(curr_proj.ftp_user or '(none)')}</code>\n"
f"• 📂 <b>Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 🔒 <b>Protocol:</b> {'Secure FTPS/TLS 🔒' if curr_proj.ftp_tls else 'Standard FTP'}\n\n"
f"📋 <b>Status:</b>\n{test_msg}"
)
try:
await query.message.reply_html(res_card, disable_web_page_preview=True)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات FTP هنوز برای این پروژه تنظیم نشده است.", show_alert=True)
except Exception:
pass
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_deploy":
await query.answer("🚀 در حال استقرار نسخه پروداکشن روی FTP..." if is_fa else "🚀 Deploying production to FTP...")
if curr_proj and curr_proj.ftp_host:
status_msg = await query.message.reply_html("🚀 <i>در حال آماده‌سازی و ارسال نسخه شاخه production به سرور FTP...</i>" if is_fa else "🚀 <i>Preparing and uploading production branch files to FTP...</i>")
res = await ftp_manager.deploy_project(
workspace_path=curr_proj.workspace,
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user or "",
password=curr_proj.ftp_password or "",
remote_path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
branch="production",
)
if res.get("success"):
files_up = res.get("files_uploaded", 0)
files_skip = res.get("files_skipped", 0)
dur = res.get("duration", 0)
b_trans = res.get("bytes_transferred", 0)
size_mb = f"{b_trans / (1024 * 1024):.2f} MB" if b_trans > 1024 * 1024 else f"{b_trans / 1024:.1f} KB"
report = (
f"✅ <b>استقرار روی FTP با موفقیت انجام شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>شاخه سورس دیپلوی:</b> <code>production</code>\n"
f"• 🌐 <b>سرور مقصد:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 📂 <b>مسیر هاست:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 📦 <b>تعداد فایل‌های آپلود شده:</b> {files_up}\n"
f"• 🧹 <b>فایل‌های زائد و فیلتر شده:</b> {files_skip}\n"
f"• 📊 <b>حجم کل منتقل شده:</b> {size_mb}\n"
f"• ⏱️ <b>مدت زمان دیپلوی:</b> {dur} ثانیه"
) if is_fa else (
f"✅ <b>FTP Deployment Completed Successfully!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>Source Branch:</b> <code>production</code>\n"
f"• 🌐 <b>Target Server:</b> <code>{escape_html(curr_proj.ftp_host)}:{curr_proj.ftp_port}</code>\n"
f"• 📂 <b>Remote Path:</b> <code>{escape_html(curr_proj.ftp_path or '/')}</code>\n"
f"• 📦 <b>Files Uploaded:</b> {files_up}\n"
f"• 🧹 <b>Excluded/Skipped:</b> {files_skip}\n"
f"• 📊 <b>Transferred:</b> {size_mb}\n"
f"• ⏱️ <b>Duration:</b> {dur}s"
)
try:
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
err = res.get("error", "Error")
err_report = (
f"❌ <b>خطا در استقرار روی FTP:</b>\n\n"
f"• ⚠️ <b>پیام خطا:</b> {escape_html(str(err))}\n"
f"• 💡 <i>پیشنهاد: ابتدا دکمه تست اتصال را بزنید یا مشخصات هاست و مسیر را بررسی کنید.</i>"
) if is_fa else f"❌ <b>FTP Deployment Error:</b>\n{escape_html(str(err))}"
try:
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات FTP تنظیم نشده است. ابتدا اطلاعات FTP را ثبت کنید.", show_alert=True)
except Exception:
pass
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ssh_menu":
text, markup = await build_ssh_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("ssh_set:"):
field_name = data.split(":")[1]
if not curr_proj:
await query.answer("⚠️ پروژه‌ای یافت نشد." if is_fa else "⚠️ No project found.", show_alert=True)
return
USER_INPUT_STATES[chat_id] = {
"type": "ssh_set",
"field": field_name,
"project": curr_proj.name,
}
field_prompts = {
"host": (
"🌐 <b>لطفاً آدرس هاست یا IP سرور SSH را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>server.mysite.com</code> یا <code>185.120.30.40:22</code>"
) if is_fa else (
"🌐 <b>Please enter the SSH Host/IP address:</b>\n\n"
"<i>Example:</i> <code>server.mysite.com</code> or <code>185.120.30.40:22</code>"
),
"user": (
"👤 <b>لطفاً نام کاربری اتصال SSH را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>root</code> یا <code>ubuntu</code> یا <code>deployer</code>"
) if is_fa else (
"👤 <b>Please enter the SSH Username:</b>\n\n"
"<i>Example:</i> <code>root</code> or <code>ubuntu</code>"
),
"pass": (
"🔑 <b>لطفاً رمز عبور (Password) اتصال SSH را ارسال کنید:</b>\n\n"
"<i>(پیام ارسالی بلافاصله پس از ثبت جهت امنیت حذف خواهد شد)</i>"
) if is_fa else (
"🔑 <b>Please enter the SSH Password:</b>\n\n"
"<i>(Message will be deleted automatically for security)</i>"
),
"key": (
"🗝️ <b>لطفاً محتوای کلید خصوصی SSH Key (مانند OpenSSH/RSA/Ed25519) یا مسیر فایل آن را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>-----BEGIN OPENSSH PRIVATE KEY----- ...</code>\n"
"<i>(پیام ارسالی بلافاصله جهت امنیت حذف خواهد شد)</i>"
) if is_fa else (
"🗝️ <b>Please send your SSH Private Key content or file path:</b>\n\n"
"<i>(Message will be deleted automatically for security)</i>"
),
"path": (
"📂 <b>لطفاً مسیر پوشه ریموت مقصد روی سرور را ارسال کنید:</b>\n\n"
"<i>مثال:</i> <code>/var/www/myproject</code> یا <code>/home/ubuntu/app</code>"
) if is_fa else (
"📂 <b>Please enter the remote directory path on the server:</b>\n\n"
"<i>Example:</i> <code>/var/www/myproject</code>"
),
}
prompt_text = field_prompts.get(field_name, "لطفاً مقدار جدید را ارسال کنید:")
cancel_kb = InlineKeyboardMarkup([[InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_ssh_menu")]])
await query.edit_message_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=cancel_kb)
elif data == "btn_ssh_exec_prompt":
if not curr_proj:
await query.answer("⚠️ پروژه‌ای یافت نشد." if is_fa else "⚠️ No project found.", show_alert=True)
return
if not curr_proj.ssh_host:
await query.answer("⚠️ ابتدا باید مشخصات هاست و سرور SSH را تنظیم نمایید.", show_alert=True)
return
USER_INPUT_STATES[chat_id] = {
"type": "ssh_exec",
"project": curr_proj.name,
}
msg = (
"💻 <b>اجرای فرمان در سرور ریموت SSH:</b>\n\n"
f"• 🌐 <b>سرور:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n\n"
"لطفاً دستور مورد نظر خود را در یک پیام ارسال کنید:\n"
"<i>مثال:</i> <code>php -v</code> یا <code>git status</code> یا <code>systemctl status nginx</code>"
) if is_fa else (
"💻 <b>Run Remote SSH Command:</b>\n\n"
f"• 🌐 <b>Server:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>Path:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n\n"
"Please send the shell command to execute:"
)
cancel_kb = InlineKeyboardMarkup([[InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_ssh_menu")]])
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=cancel_kb)
elif data == "ssh_clear_conf":
confirm_text = (
"🗑️ <b>آیا از پاک‌سازی کامل اطلاعات سرور SSH این پروژه اطمینان دارید؟</b>\n\n"
"تمام مشخصات هاست، نام کاربری، رمز عبور و کلید SSH حذف خواهند شد."
) if is_fa else (
"🗑️ <b>Are you sure you want to clear SSH settings for this project?</b>"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، پاک شود" if is_fa else "💥 Yes, Clear", callback_data="ssh_clear_do"),
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data="btn_ssh_menu"),
]
]
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "ssh_clear_do":
if curr_proj:
curr_proj.ssh_host = None
curr_proj.ssh_port = 22
curr_proj.ssh_user = None
curr_proj.ssh_password = None
curr_proj.ssh_key = None
curr_proj.ssh_path = "/"
session_manager.save()
remote_audit.log_operation(
user_id=chat_id,
project_name=curr_proj.name,
service_type="ssh",
action_type="update_config",
requester="user_button",
status="success",
details="Cleared SSH config",
result_summary="SSH configuration cleared",
)
await query.answer("🗑️ اطلاعات SSH با موفقیت پاک شد." if is_fa else "🗑️ SSH settings cleared.")
text, markup = await build_ssh_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ssh_test":
await query.answer("🔍 در حال بررسی اتصال SSH..." if is_fa else "🔍 Testing SSH connection...")
if curr_proj and curr_proj.ssh_host:
test_ok, test_msg = await ssh_manager.test_connection(
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user or "",
password=curr_proj.ssh_password,
key=curr_proj.ssh_key,
path=curr_proj.ssh_path or "/",
user_id=chat_id,
project_name=curr_proj.name,
requester="user_button",
)
icon = "✅" if test_ok else "❌"
res_card = (
f"{icon} <b>نتیجه تست اتصال SSH:</b>\n\n"
f"• 🌐 <b>هاست:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 👤 <b>نام کاربری:</b> <code>{escape_html(curr_proj.ssh_user or '(بدون نام کاربری)')}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n\n"
f"📋 <b>گزارش ارتباط:</b>\n{test_msg}"
) if is_fa else (
f"{icon} <b>SSH Connection Test Result:</b>\n\n"
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 👤 <b>User:</b> <code>{escape_html(curr_proj.ssh_user or '(none)')}</code>\n"
f"• 📂 <b>Path:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n\n"
f"📋 <b>Status:</b>\n{test_msg}"
)
try:
await query.message.reply_html(res_card, disable_web_page_preview=True)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات SSH هنوز برای این پروژه تنظیم نشده است.", show_alert=True)
except Exception:
pass
text, markup = await build_ssh_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ssh_push":
await query.answer("🚀 در حال ارسال شاخه پروداکشن به سرور SSH..." if is_fa else "🚀 Pushing production to SSH...")
if curr_proj and curr_proj.ssh_host:
status_msg = await query.message.reply_html("🚀 <i>در حال ارسال کدهای شاخه production به سرور SSH...</i>" if is_fa else "🚀 <i>Uploading production branch files via SFTP...</i>")
res = await ssh_manager.push_project(
workspace_path=curr_proj.workspace,
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user or "",
password=curr_proj.ssh_password,
key=curr_proj.ssh_key,
remote_path=curr_proj.ssh_path or "/",
branch="production",
user_id=chat_id,
project_name=curr_proj.name,
requester="user_button",
)
if res.get("success"):
files_up = res.get("files_uploaded", 0)
files_skip = res.get("files_skipped", 0)
dur = round(res.get("duration", 0), 2)
b_trans = res.get("bytes_transferred", 0)
size_mb = f"{b_trans / (1024 * 1024):.2f} MB" if b_trans > 1024 * 1024 else f"{b_trans / 1024:.1f} KB"
report = (
f"✅ <b>پوش به سرور پروداکشن با موفقیت انجام شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>شاخه مبدأ:</b> <code>production</code>\n"
f"• 🌐 <b>سرور مقصد:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>مسیر سرور:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n"
f"• 📦 <b>تعداد فایل‌های آپلود شده:</b> {files_up}\n"
f"• 🛡️ <b>فایل‌های محلی/دیتابیس فیلتر شده:</b> {files_skip}\n"
f"• 🔒 <b>داده‌های سرور پروداکشن:</b> کاملاً دست‌نخورده و ایمن حفظ شدند\n"
f"• 📊 <b>حجم کل منتقل شده:</b> {size_mb}\n"
f"• ⏱️ <b>مدت زمان انتقال:</b> {dur} ثانیه"
) if is_fa else (
f"✅ <b>SSH Push Completed Successfully!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌿 <b>Source Branch:</b> <code>production</code>\n"
f"• 🌐 <b>Target Server:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>Remote Path:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n"
f"• 📦 <b>Files Uploaded:</b> {files_up}\n"
f"• 🛡️ <b>Skipped Junk/Databases:</b> {files_skip}\n"
f"• 🔒 <b>Remote Production Data:</b> Safe & Untouched\n"
f"• 📊 <b>Transferred:</b> {size_mb}\n"
f"• ⏱️ <b>Duration:</b> {dur}s"
)
try:
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
err = res.get("error", "Error")
err_report = (
f"❌ <b>خطا در پوش به سرور SSH:</b>\n\n"
f"• ⚠️ <b>پیام خطا:</b> {escape_html(str(err))}\n"
f"• 💡 <i>پیشنهاد: ابتدا دکمه تست اتصال را بررسی کنید یا دسترسی‌های دایرکتوری سرور را چک فرمایید.</i>"
) if is_fa else f"❌ <b>SSH Push Error:</b>\n{escape_html(str(err))}"
try:
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات SSH تنظیم نشده است.", show_alert=True)
except Exception:
pass
text, markup = await build_ssh_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ssh_pull":
await query.answer("📥 در حال دریافت فایل‌ها از سرور SSH..." if is_fa else "📥 Pulling files from SSH server...")
if curr_proj and curr_proj.ssh_host:
status_msg = await query.message.reply_html("📥 <i>در حال دانلود فایل‌های پروژه از سرور SSH...</i>" if is_fa else "📥 <i>Downloading files via SFTP...</i>")
res = await ssh_manager.pull_project(
workspace_path=curr_proj.workspace,
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user or "",
password=curr_proj.ssh_password,
key=curr_proj.ssh_key,
remote_path=curr_proj.ssh_path or "/",
branch="production",
user_id=chat_id,
project_name=curr_proj.name,
requester="user_button",
)
if res.get("success"):
files_down = res.get("files_downloaded", 0)
files_skip = res.get("files_skipped", 0)
dur = round(res.get("duration", 0), 2)
b_trans = res.get("bytes_transferred", 0)
size_mb = f"{b_trans / (1024 * 1024):.2f} MB" if b_trans > 1024 * 1024 else f"{b_trans / 1024:.1f} KB"
report = (
f"✅ <b>دریافت فایل‌ها (Pull) با موفقیت انجام شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>سرور مبدأ:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>مسیر سرور:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n"
f"• 📦 <b>تعداد فایل‌های دریافت شده:</b> {files_down}\n"
f"• 🛡️ <b>فایل‌های نادیده گرفته‌شده (دیتابیس/لاگ):</b> {files_skip}\n"
f"• 📊 <b>حجم کل دانلود شده:</b> {size_mb}\n"
f"• ⏱️ <b>مدت زمان انتقال:</b> {dur} ثانیه"
) if is_fa else (
f"✅ <b>SSH Pull Completed Successfully!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>Source Server:</b> <code>{escape_html(curr_proj.ssh_host)}:{curr_proj.ssh_port}</code>\n"
f"• 📂 <b>Remote Path:</b> <code>{escape_html(curr_proj.ssh_path or '/')}</code>\n"
f"• 📦 <b>Files Downloaded:</b> {files_down}\n"
f"• 🛡️ <b>Skipped (Databases/Logs):</b> {files_skip}\n"
f"• 📊 <b>Transferred:</b> {size_mb}\n"
f"• ⏱️ <b>Duration:</b> {dur}s"
)
try:
await status_msg.edit_text(report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
err = res.get("error", "Error")
err_report = (
f"❌ <b>خطا در دریافت از سرور SSH:</b>\n\n"
f"• ⚠️ <b>پیام خطا:</b> {escape_html(str(err))}\n"
) if is_fa else f"❌ <b>SSH Pull Error:</b>\n{escape_html(str(err))}"
try:
await status_msg.edit_text(err_report, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات SSH تنظیم نشده است.", show_alert=True)
except Exception:
pass
text, markup = await build_ssh_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ssh_logs":
await query.answer("📜 در حال دریافت تاریخچه..." if is_fa else "📜 Loading logs...")
proj_name = curr_proj.name if curr_proj else "default"
logs = remote_audit.get_recent_logs(user_id=chat_id, project_name=proj_name, limit=12)
log_text = remote_audit.format_logs_for_tg(logs, project_name=proj_name, is_fa=is_fa)
back_kb = InlineKeyboardMarkup([[InlineKeyboardButton("🔙 بازگشت به منوی SSH" if is_fa else "🔙 Back to SSH", callback_data="btn_ssh_menu")]])
try:
await query.edit_message_text(log_text, parse_mode=constants.ParseMode.HTML, reply_markup=back_kb)
except Exception:
await query.message.reply_html(log_text, reply_markup=back_kb)
elif data == "btn_git_sync":
await query.answer("🔄 در حال همگام‌سازی با Gitea..." if is_fa else "🔄 Syncing with Gitea...")
if curr_proj:
await git_manager.git_sync(curr_proj.workspace, message="Sync from bot UI", repo_name=curr_proj.name)
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_commit":
await query.answer("💾 در حال ثبت کامیت و ارسال..." if is_fa else "💾 Committing and pushing...")
if curr_proj:
await git_manager.git_commit_and_push(curr_proj.workspace, message=f"Commit from bot UI for {curr_proj.name}", repo_name=curr_proj.name)
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_pull":
await query.answer("⬇️ در حال دریافت آخرین تغییرات..." if is_fa else "⬇️ Pulling latest changes...")
if curr_proj:
await git_manager.git_pull(curr_proj.workspace)
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("git_hist:"):
page_str = data.split(":", 1)[1]
try:
page_num = int(page_str)
except ValueError:
page_num = 1
await query.answer("📜 در حال دریافت تاریخچه کامیت‌ها..." if is_fa else "📜 Loading commit history...")
text, markup = await build_git_history_menu(chat_id, page=page_num)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("git_view:"):
commit_hash = data.split(":", 1)[1]
await query.answer("🔍 در حال بارگذاری مشخصات کامیت..." if is_fa else "🔍 Loading commit details...")
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("git_co:"):
target_ref = data.split(":", 1)[1]
await query.answer("🔀 در حال سوییچ به کامیت..." if is_fa else "🔀 Switching to commit...")
if curr_proj:
ok, co_msg = await git_manager.git_checkout(curr_proj.workspace, target_ref)
if ok:
await query.answer("✅ " + co_msg, show_alert=True)
else:
await query.answer("❌ " + co_msg, show_alert=True)
text, markup = await build_git_commit_detail_menu(chat_id, target_ref)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "git_co_main":
await query.answer("🌿 در حال بازگشت به شاخه اصلی (main)..." if is_fa else "🌿 Returning to main...")
if curr_proj:
ok, co_msg = await git_manager.git_checkout(curr_proj.workspace, "main")
if ok:
await query.answer("✅ سوییچ به شاخه اصلی (main) با موفقیت انجام شد." if is_fa else "✅ Switched to main branch.", show_alert=True)
else:
await query.answer("❌ " + co_msg, show_alert=True)
text, markup = await build_git_history_menu(chat_id, page=1)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("git_ask_reset:"):
commit_hash = data.split(":", 1)[1]
confirm_text = (
f"⚠️ <b>هشدار بازنشانی و بازگشت پروژه (Git Hard Reset)</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید وضعیت فایل‌ها و پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> را دقیقاً به کامیت <code>{escape_html(commit_hash)}</code> بازگردانید؟\n\n"
f"<i>کلیه تغییرات بعد از این کامیت بازنشانی خواهند شد.</i>"
if is_fa else
f"⚠️ <b>Warning: Git Hard Reset</b>\n\n"
f"Are you sure you want to reset project <code>{escape_html(curr_proj.name if curr_proj else '')}</code> to commit <code>{escape_html(commit_hash)}</code>?\n\n"
f"<i>All uncommitted or subsequent changes will be reset.</i>"
)
confirm_markup = InlineKeyboardMarkup([
[
InlineKeyboardButton("✅ بله، بازگردانی شود" if is_fa else "✅ Yes, Reset to this commit", callback_data=f"git_do_reset:{commit_hash}"),
],
[
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data=f"git_view:{commit_hash}"),
],
])
try:
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
except Exception:
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
elif data.startswith("git_do_reset:"):
commit_hash = data.split(":", 1)[1]
await query.answer("⏪ در حال بازگردانی به کامیت..." if is_fa else "⏪ Resetting to commit...")
if curr_proj:
ok, reset_msg = await git_manager.git_reset_hard(curr_proj.workspace, commit_hash)
if ok:
await query.answer("✅ بازگردانی با موفقیت انجام شد." if is_fa else "✅ Reset completed successfully.", show_alert=True)
else:
await query.answer("❌ " + reset_msg, show_alert=True)
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "git_ask_undo_head":
confirm_text = (
f"⚠️ <b>تأیید لغو آخرین تغییرات (Git Revert HEAD)</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید آخرین کامیت ثبت‌شده در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> را لغو کنید؟\n\n"
f"<i>یک کامیت معکوس ایجاد خواهد شد و تاریخچهٔ پروژه با امنیت کامل حفظ می‌شود.</i>"
if is_fa else
f"⚠️ <b>Confirm Undo Last Commit (Git Revert HEAD)</b>\n\n"
f"Are you sure you want to revert the latest commit on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
f"<i>A safe reverse commit will be created preserving your Git history.</i>"
)
confirm_markup = InlineKeyboardMarkup([
[
InlineKeyboardButton("↩️ بله، لغو شود (Revert)" if is_fa else "↩️ Yes, Revert HEAD", callback_data="git_do_undo_head"),
],
[
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_git_menu"),
],
])
try:
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
except Exception:
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
elif data == "git_do_undo_head":
await query.answer("↩️ در حال لغو آخرین تغییرات..." if is_fa else "↩️ Reverting latest commit...")
if curr_proj:
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target="HEAD")
if ok:
await query.answer("✅ آخرین تغییر با موفقیت لغو شد." if is_fa else "✅ Successfully reverted latest commit.", show_alert=True)
else:
await query.answer("❌ " + res_msg, show_alert=True)
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("git_ask_revert:"):
commit_hash = data.split(":", 1)[1]
confirm_text = (
f"⚠️ <b>تأیید لغو کامیت (Git Revert)</b>\n\n"
f"آیا می‌خواهید تغییرات اعمال‌شده در کامیت <code>{escape_html(commit_hash)}</code> را در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> لغو کنید؟\n\n"
f"<i>تغییرات این کامیت به‌صورت یک کامیت معکوس جدید ثبت خواهد شد.</i>"
if is_fa else
f"⚠️ <b>Confirm Git Revert</b>\n\n"
f"Are you sure you want to revert commit <code>{escape_html(commit_hash)}</code> on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
f"<i>Changes from this commit will be inverted in a new commit.</i>"
)
confirm_markup = InlineKeyboardMarkup([
[
InlineKeyboardButton("↩️ بله، لغو شود" if is_fa else "↩️ Yes, Revert", callback_data=f"git_do_revert:{commit_hash}"),
],
[
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data=f"git_view:{commit_hash}"),
],
])
try:
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
except Exception:
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
elif data.startswith("git_do_revert:"):
commit_hash = data.split(":", 1)[1]
await query.answer("↩️ در حال لغو کامیت..." if is_fa else "↩️ Reverting commit...")
if curr_proj:
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_hash)
if ok:
await query.answer("✅ کامیت با موفقیت لغو شد." if is_fa else "✅ Commit reverted successfully.", show_alert=True)
else:
await query.answer("❌ " + res_msg, show_alert=True)
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data.startswith("aibtn:"):
btn_id = data.split(":", 1)[1].strip()
from bot_actions import get_ai_button_payload
payload_data = get_ai_button_payload(btn_id)
if not payload_data:
await query.answer("⚠️ این دکمه منقضی شده یا نامعتبر است." if is_fa else "⚠️ This button has expired or is invalid.", show_alert=True)
return
act_type = payload_data.get("type", "prompt")
act_value = payload_data.get("value", "").strip()
btn_project = payload_data.get("project")
if act_type == "prompt":
await query.answer("🧠 در حال ارسال دستور به هوش مصنوعی..." if is_fa else "🧠 Processing prompt with AI...")
if btn_project:
accessible = session_manager.get_all_accessible_projects(chat_id)
for k, p in accessible.items():
if k.lower() == btn_project.lower() or p.name.lower() == btn_project.lower():
session.active_project = p.name
session_manager.save()
break
prompt_echo = (
f"💬 <b>دستور ارسالی:</b> <code>{escape_html(act_value)}</code>"
if is_fa else
f"💬 <b>Sent prompt:</b> <code>{escape_html(act_value)}</code>"
)
try:
await query.message.reply_html(prompt_echo)
except Exception:
pass
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, act_value))
elif act_type == "cmd":
await query.answer("⚡ در حال اجرای دستور..." if is_fa else "⚡ Running command...")
cmd = act_value
if cmd.startswith("/"):
cmd_parts = cmd.split(maxsplit=1)
c_name = cmd_parts[0].lower()
if c_name in ("/usage", "/quota", "/credits", "/credit"):
text, markup = await build_usage_report(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/status", "/stats", "/sys"):
text, markup = build_server_hardware_menu(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/git", "/gitea"):
text, markup = await build_git_menu(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/tasks", "/cron", "/schedule"):
text, markup = build_tasks_menu(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/memory", "/mem"):
text, markup = build_memory_menu(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/projects", "/proj"):
text, markup = build_projects_menu(chat_id)
await query.message.reply_html(text, reply_markup=markup)
elif c_name in ("/backup", "/export"):
if curr_proj:
await query.message.reply_html("📦 در حال تهیه فایل پشتیبان..." if is_fa else "📦 Creating backup...")
zip_p = await backup_manager.create_project_backup(curr_proj.workspace, curr_proj.name)
if zip_p and zip_p.exists():
await context.bot.send_document(
chat_id=chat_id,
document=zip_p.open("rb"),
filename=zip_p.name,
caption=f"📦 Backup of <b>{escape_html(curr_proj.name)}</b>",
parse_mode=constants.ParseMode.HTML,
)
else:
await query.message.reply_html("❌ خطا در ایجاد پشتیبان." if is_fa else "❌ Failed to create backup.")
else:
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, cmd))
else:
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, cmd))
elif act_type == "action":
query.data = act_value
await callback_handler(update, context)
return
elif data in ("btn_usage_menu", "btn_usage_refresh"):
await query.answer("⏳ استعلام مصرف..." if is_fa else "⏳ Refreshing...")
text, markup = await build_usage_report(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data in ("btn_hw_menu", "btn_hw_refresh"):
await query.answer("⏳ استعلام سخت‌افزار سرور..." if is_fa else "⏳ Refreshing Hardware...")
text, markup = build_server_hardware_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_help_menu":
if is_fa:
text = (
f"📖 <b>راهنمای جامع ربات Antigravity (AGY)</b>\n\n"
f"<b>📁 مدیریت چند پروژه‌ای (Projects):</b>\n"
f"• <code>/projects</code> یا <code>/project</code> - پنل تعاملی مدیریت و انتخاب پروژه‌ها\n"
f"• <code>/newproject &lt;name&gt;</code> - ساخت و فعال‌سازی پروژه اختصاصی\n"
f"• <code>/switch &lt;name&gt;</code> - سوییچ بین پروژه‌ها\n"
f"• <code>/delproject &lt;name&gt;</code> - حذف یک پروژه\n\n"
f"<b>🤝 اشتراک‌گذاری پروژه (Project Sharing):</b>\n"
f"• <code>/share &lt;user_id&gt;</code> - اشتراک‌گذاری پروژه فعال\n"
f"• <code>/unshare &lt;user_id&gt;</code> - لغو دسترسی کاربر\n"
f"• <code>/shared</code> - لیست پروژه‌های اشتراکی\n\n"
f"<b>💬 مدیریت گفتگو:</b>\n"
f"• <code>/new</code> - ری‌استارت گفتگو در پروژه فعال\n"
f"• <code>/status</code> - مشاهده وضعیت سیستم و مدل\n"
f"• <code>/usage</code> - گزارش سهمیه و مصرف AGY\n"
f"• <code>/cancel</code> - لغو پردازش فعال\n"
)
keyboard = [
[
InlineKeyboardButton("📁 پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("📈 سهمیه مصرف", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 بستن", callback_data="proj_close"),
],
]
else:
text = (
f"📖 <b>Antigravity (AGY) Quick Reference</b>\n\n"
f"<b>📁 Multi-Project:</b>\n"
f"• <code>/projects</code> - Interactive Project Manager\n"
f"• <code>/newproject &lt;name&gt;</code> - Create & activate project\n"
f"• <code>/switch &lt;name&gt;</code> - Switch active project\n\n"
f"<b>🤝 Sharing:</b>\n"
f"• <code>/share &lt;user_id&gt;</code> - Share active project\n"
f"• <code>/unshare &lt;user_id&gt;</code> - Revoke user access\n"
f"• <code>/shared</code> - View shared projects\n\n"
f"<b>💬 Conversation:</b>\n"
f"• <code>/new</code> - Reset chat in project\n"
f"• <code>/usage</code> - Check AGY quotas\n"
f"• <code>/cancel</code> - Stop active task\n"
)
keyboard = [
[
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
],
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "btn_settings_menu":
text, markup = build_settings_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_menu":
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_start_login":
await query.answer("⏳ در حال ایجاد نشست احراز هویت گوگل..." if is_fa else "⏳ Starting Google OAuth session...")
success, msg, auth_url = await auth_manager.start_oauth_login(chat_id)
if not success or not auth_url:
await query.message.reply_html(f"❌ {msg}")
return
if is_fa:
prompt_text = (
"🔑 <b>ورود به حساب اختصاصی گوگل (AGY OAuth Login)</b>\n\n"
"۱. روی دکمه زیر کلیک کنید تا صفحه رسمی گوگل باز شود.\n"
"۲. وارد حساب کاربری گوگل خود شوید و اجازه دسترسی را تایید نمایید.\n"
"۳. <b>کد تایید احراز هویت (Authorization Code)</b> نمایش داده شده را کپی کرده و در چت ارسال کنید.\n\n"
"⏳ <i>فرصت ارسال کد: ۶۰ ثانیه</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 ورود به حساب کاربری گوگل", url=auth_url)],
[InlineKeyboardButton("❌ انصراف / لغو", callback_data="btn_account_cancel_login")],
]
else:
prompt_text = (
"🔑 <b>Google Account Login (AGY OAuth)</b>\n\n"
"1. Click the button below to open Google's authorization page.\n"
"2. Sign in to your Google Account and grant access permissions.\n"
"3. Copy the <b>Authorization Code</b> and paste it in chat.\n\n"
"⏳ <i>Timeout: 60 seconds</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 Sign in with Google", url=auth_url)],
[InlineKeyboardButton("❌ Cancel", callback_data="btn_account_cancel_login")],
]
await query.message.reply_html(prompt_text, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
elif data == "btn_account_cancel_login":
auth_manager.cancel_oauth_login(chat_id)
await query.answer("فرآیند لاگین لغو شد." if is_fa else "Login canceled.")
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_logout":
success, msg = auth_manager.logout_user(chat_id)
await query.answer(msg, show_alert=True)
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_copy_server":
await query.answer("⛔ حساب‌های اشتراکی غیرفعال شده‌اند و استفاده از حساب اختصاصی الزامی است.", show_alert=True)
elif data == "btn_status_menu":
accessible = session_manager.get_all_accessible_projects(chat_id)
if not curr_proj:
text = "⚠️ هیچ پروژه‌ای فعال نیست." if is_fa else "⚠️ No active project."
keyboard = [[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]]
else:
try:
total, used, free = shutil.disk_usage(curr_proj.workspace)
disk_free_gb = f"{free / (1024**3):.1f} GB"
except Exception:
disk_free_gb = "N/A"
ctx_info = f"{curr_proj.last_context_length:,}" if curr_proj.last_context_length else "0"
tok_info = f"{curr_proj.last_total_tokens:,}" if curr_proj.last_total_tokens else "0"
if is_fa:
text = (
f"📊 <b>وضعیت سیستم و نشست کاربری</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 📂 <b>دایرکتوری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
f"• 📏 <b>طول کانتکست:</b> <code>{ctx_info} توکن</code>\n"
f"• 🪙 <b>مجموع توکن‌ها:</b> <code>{tok_info}</code>\n"
f"• 💾 <b>فضای آزاد دیسک:</b> <code>{disk_free_gb}</code>\n"
f"• ⚙️ <b>وضعیت پردازش:</b> {'⏳ در حال اجرا' if session.turn_in_progress else '🟢 آماده'}\n"
f"• 📁 <b>تعداد کل پروژه‌ها:</b> <code>{len(accessible)}</code>"
)
keyboard = [
[
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
InlineKeyboardButton("📈 سهمیه مصرف AGY", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"📊 <b>System & Session Status</b>\n\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>Effort:</b> <code>{curr_proj.effort}</code>\n"
f"• 📏 <b>Context:</b> <code>{ctx_info} tokens</code>\n"
f"• 🪙 <b>Total Tokens:</b> <code>{tok_info}</code>\n"
f"• 💾 <b>Disk Free:</b> <code>{disk_free_gb}</code>\n"
f"• 📁 <b>Accessible Projects:</b> <code>{len(accessible)}</code>"
)
keyboard = [
[
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
],
[
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "btn_effort_menu":
if not curr_proj:
await query.answer("پروژه‌ای فعال نیست.", show_alert=True)
return
keyboard = []
for e in AVAILABLE_EFFORTS:
is_selected = "✅ " if (curr_proj and e == curr_proj.effort) else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{e.capitalize()} Effort", callback_data=f"set_effort:{e}")])
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
title = (
f"⚡ <b>تنظیم سطح استدلال و تفکر (Reasoning Effort)</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>سطح فعلی:</b> <code>{curr_proj.effort}</code>"
if is_fa else
f"⚡ <b>Set Reasoning Effort</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• ⚡ <b>Current Effort:</b> <code>{curr_proj.effort}</code>"
)
try:
await query.edit_message_text(title, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(title, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "btn_lang_menu":
keyboard = []
row = []
for code, label in AVAILABLE_LANGUAGES.items():
is_selected = "✅ " if (session.language.lower() == code.lower() or session.language == label) else ""
row.append(InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_lang:{code}"))
if len(row) == 2:
keyboard.append(row)
row = []
if row:
keyboard.append(row)
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
curr_display = get_lang_display(session.language)
title = f"🌐 <b>انتخاب زبان پاسخ‌دهی</b>\nزبان فعلی: <code>{escape_html(curr_display)}</code>" if is_fa else f"🌐 <b>Select Response Language</b>\nCurrent: <code>{escape_html(curr_display)}</code>"
try:
await query.edit_message_text(title, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(title, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("set_model:"):
model_id = data.split(":", 1)[1]
await session_manager.set_model(chat_id, model_id)
curr_proj = session_manager.get_current_project(chat_id)
keyboard = []
for mid, label in AVAILABLE_MODELS.items():
is_selected = "✅ " if (curr_proj and mid == curr_proj.model) else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_model:{mid}")])
keyboard.append([
InlineKeyboardButton("⚙️ تنظیمات" if is_fa else "⚙️ Settings", callback_data="btn_settings_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
])
proj_name_str = curr_proj.name if curr_proj else ""
await query.edit_message_text(
f"🧠 <b>Model selected for {escape_html(proj_name_str)}:</b> <code>{model_id}</code>",
parse_mode=constants.ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
elif data.startswith("set_effort:"):
eff = data.split(":", 1)[1]
await session_manager.set_effort(chat_id, eff)
curr_proj = session_manager.get_current_project(chat_id)
keyboard = []
for e in AVAILABLE_EFFORTS:
is_selected = "✅ " if (curr_proj and e == curr_proj.effort) else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{e.capitalize()} Effort", callback_data=f"set_effort:{e}")])
keyboard.append([
InlineKeyboardButton("⚙️ تنظیمات" if is_fa else "⚙️ Settings", callback_data="btn_settings_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
proj_name_str = curr_proj.name if curr_proj else ""
await query.edit_message_text(
f"⚡ <b>Effort selected for {escape_html(proj_name_str)}:</b> <code>{eff}</code>",
parse_mode=constants.ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
elif data.startswith("set_lang:"):
lang_code = data.split(":", 1)[1]
await session_manager.set_language(chat_id, lang_code)
session = session_manager.get_or_create(chat_id)
keyboard = []
row = []
for code, label in AVAILABLE_LANGUAGES.items():
is_selected = "✅ " if (session.language.lower() == code.lower() or session.language == label) else ""
row.append(InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_lang:{code}"))
if len(row) == 2:
keyboard.append(row)
row = []
if row:
keyboard.append(row)
keyboard.append([
InlineKeyboardButton("⚙️ تنظیمات" if is_fa else "⚙️ Settings", callback_data="btn_settings_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
])
curr_display = get_lang_display(session.language)
await query.edit_message_text(
f"🌐 <b>Response language selected:</b> <code>{escape_html(curr_display)}</code>\n\n"
f"AGY will now always respond using this language.",
parse_mode=constants.ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
elif data in ("btn_stop", "btn_cancel"):
session = session_manager.get_or_create(chat_id)
cancelled = session_manager.cancel_active_task(chat_id)
session.turn_in_progress = False
session_manager.save()
if cancelled:
try:
await query.answer("🛑 عملیات متوقف شد." if is_fa else "🛑 Task stopped.", show_alert=True)
except Exception:
pass
msg = "🛑 <b>عملیات و پردازش فعلی متوقف شد.</b>" if is_fa else "🛑 <b>Active task was stopped.</b>"
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=None)
except Exception:
try:
await query.message.reply_html(msg)
except Exception:
pass
else:
try:
await query.answer("ℹ️ هیچ عملیات فعالی برای توقف وجود ندارد." if is_fa else "️ No active task to stop.", show_alert=True)
except Exception:
pass
try:
await query.edit_message_reply_markup(reply_markup=None)
except Exception:
pass
elif data == "btn_compact":
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
await query.answer("🗜️ در حال فشرده‌سازی کانتکست..." if is_fa else "🗜️ Compacting context...")
toks = get_conversation_context_tokens(curr_proj.conversation_id)
curr_proj.last_context_length = toks
session.last_context_length = toks
session_manager.save()
model_lower = (curr_proj.model or "").lower()
if "3.1-pro" in model_lower or "pro" in model_lower:
max_ctx = 2_000_000
elif "claude" in model_lower:
max_ctx = 200_000
elif "gpt" in model_lower:
max_ctx = 128_000
else:
max_ctx = 1_000_000
pct = (toks / max_ctx) * 100 if max_ctx else 0
pct_str = f"{pct:.1f}%"
free_pct = f"{max(0.0, 100.0 - pct):.1f}%"
if is_fa:
msg = (
f"🗜️ <b>کانتکست با موفقیت فشرده و بهینه‌سازی شد!</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>طول کانتکست فعال:</b> <code>{toks:,} توکن</code>\n"
f"• 🎯 <b>سقف پنجره:</b> <code>{max_ctx:,} توکن</code>\n"
f"• 📊 <b>میزان اشغال پنجره (CL):</b> <code>{pct_str}</code> (فضای آزاد: <code>{free_pct}</code>)\n\n"
f"✅ حافظه گفتگو بهینه‌سازی شد و برای ادامه آماده است."
)
keyboard = [
[
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
msg = (
f"🗜️ <b>Context Compacted & Optimized!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• 📥 <b>Active Context:</b> <code>{toks:,} tokens</code>\n"
f"• 🎯 <b>Max Window:</b> <code>{max_ctx:,} tokens</code>\n"
f"• 📊 <b>Usage (CL):</b> <code>{pct_str}</code> (Free: <code>{free_pct}</code>)\n\n"
f"✅ Context memory is optimized and ready."
)
keyboard = [
[
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "btn_restart":
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
await session_manager.reset_session(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
await query.answer("🔄 Conversation restarted.")
if is_fa:
msg = (
f"🔄 <b>گفتگوی پروژه ری‌استارت شد!</b>\n\n"
f"حافظه و مکالمه جدید برای پروژه شروع شد.\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
f"• 🌐 <b>زبان:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>در صورت تمایل به بازگشت به گفتگوی قبلی، از دکمه زیر استفاده کنید:</i>"
)
keyboard = [
[
InlineKeyboardButton("⏮️ باز کردن گفتگوی قبلی", callback_data="btn_conv_last"),
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
msg = (
f"🔄 <b>Session Restarted!</b>\n\n"
f"Started a new AGY conversation for project.\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
f"💡 <i>To return to the previous conversation, click below:</i>"
)
keyboard = [
[
InlineKeyboardButton("⏮️ Reopen Previous Chat", callback_data="btn_conv_last"),
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "btn_conv_menu":
text, markup = build_conversations_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_conv_last":
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
success, msg_text, conv_id, meta = session_manager.reopen_last_conversation(chat_id)
if not success:
await query.answer(strip_ansi(msg_text), show_alert=True)
return
await query.answer("⏮️ آخرین گفتگو بازیابی شد.")
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
turns = meta.get("turns_count", 0)
if is_fa:
text = (
f"⏮️ <b>آخرین گفتگو با موفقیت بازیابی و فعال شد!</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>شناسه مکالمه:</b> <code>{conv_id}</code>\n"
f"• 🔢 <b>تعداد نوبت‌ها:</b> <code>{turns} نوبت</code>\n"
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>پیام بعدی شما در ادامه همین گفتگو پردازش خواهد شد.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"⏮️ <b>Last Conversation Reopened!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Conversation ID:</b> <code>{conv_id}</code>\n"
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>Your next prompt will continue this conversation.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("conv_switch_"):
target_cid = data[len("conv_switch_"):]
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
success, msg_text, meta = session_manager.switch_conversation(chat_id, target_cid)
if not success:
await query.answer(strip_ansi(msg_text), show_alert=True)
return
curr_proj = session_manager.get_current_project(chat_id)
await query.answer("✅ گفتگو با موفقیت تغییر یافت.")
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
turns = meta.get("turns_count", 0)
if is_fa:
text = (
f"💬 <b>گفتگو تغییر یافت!</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'default')}</code>\n"
f"• 💬 <b>شناسه مکالمه فعال:</b> <code>{target_cid}</code>\n"
f"• 🔢 <b>تعداد نوبت‌ها:</b> <code>{turns} نوبت</code>\n"
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>پیام‌های بعدی شما در این گفتگو ارسال خواهند شد.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🗑️ حذف این گفتگو", callback_data=f"conv_del_ask_{target_cid}"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
text = (
f"💬 <b>Conversation Switched!</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>Active Conversation:</b> <code>{target_cid}</code>\n"
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
f"💡 <i>Your next messages will continue in this conversation.</i>"
)
keyboard = [
[
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
],
[
InlineKeyboardButton("🗑️ Delete this Chat", callback_data=f"conv_del_ask_{target_cid}"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("conv_view_"):
target_cid = data[len("conv_view_"):]
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
text, markup = build_conversation_detail_menu(chat_id, target_cid)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("conv_del_ask_"):
target_cid = data[len("conv_del_ask_"):]
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
meta = get_conversation_metadata(target_cid)
first_prompt = meta.get("first_prompt") or "(بدون متن)"
prompt_snippet = first_prompt.replace("\n", " ").strip()[:60]
if is_fa:
text = (
f"⚠️ <b>تأیید حذف گفتگو:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>شناسه:</b> <code>{target_cid}</code>\n"
f"• 📝 <b>موضوع:</b> <i>«{escape_html(prompt_snippet)}»</i>\n\n"
f"آیا از حذف کامل این گفتگو و پاکسازی اطلاعات آن مطمئن هستید؟"
)
keyboard = [
[
InlineKeyboardButton("🗑️ بله، حذف شود", callback_data=f"conv_del_confirm_{target_cid}"),
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
]
]
else:
text = (
f"⚠️ <b>Confirm Conversation Deletion:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 💬 <b>ID:</b> <code>{target_cid}</code>\n"
f"• 📝 <b>Topic:</b> <i>\"{escape_html(prompt_snippet)}\"</i>\n\n"
f"Are you sure you want to permanently delete this conversation?"
)
keyboard = [
[
InlineKeyboardButton("🗑️ Yes, Delete", callback_data=f"conv_del_confirm_{target_cid}"),
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
]
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("conv_del_confirm_"):
target_cid = data[len("conv_del_confirm_"):]
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
success, msg = session_manager.delete_conversation(chat_id, target_cid)
if success:
await query.answer("✅ گفتگو حذف شد." if is_fa else "✅ Conversation deleted.", show_alert=True)
else:
await query.answer(strip_ansi(msg)[:100], show_alert=True)
text, markup = build_conversations_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "conv_clear_all_ask":
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
if is_fa:
text = (
f"⚠️ <b>تأیید پاکسازی تمام گفتگوها:</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید تمام تاریخچه گفتگوهای پروژه <code>{escape_html(curr_proj.name)}</code> را حذف کنید؟\n\n"
f"<i>این عملیات تمام مکالمات قبلی این پروژه را پاک کرده و غیرقابل بازگشت است.</i>"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، حذف همه گفتگوها", callback_data="conv_clear_all_confirm"),
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
]
]
else:
text = (
f"⚠️ <b>Confirm Clear All Conversations:</b>\n\n"
f"Are you sure you want to delete all conversations for project <code>{escape_html(curr_proj.name)}</code>?\n\n"
f"<i>This action is permanent and cannot be undone.</i>"
)
keyboard = [
[
InlineKeyboardButton("💥 Yes, Delete All", callback_data="conv_clear_all_confirm"),
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
]
]
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "conv_clear_all_confirm":
if not curr_proj:
await query.answer("⚠️ ابتدا پروژه‌ای بسازید.", show_alert=True)
return
success, msg, count = session_manager.clear_project_conversations(chat_id)
if success:
await query.answer(f"✅ {count} گفتگو حذف شد." if is_fa else f"✅ {count} conversations deleted.", show_alert=True)
else:
await query.answer(strip_ansi(msg)[:100], show_alert=True)
text, markup = build_conversations_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_compress":
if not curr_proj:
await query.answer("⚠️ No active project.", show_alert=True)
return
if session.turn_in_progress:
await query.answer("⚠️ A task is currently running.", show_alert=True)
return
if not curr_proj.conversation_id and not curr_proj.last_context_length:
await query.answer("️ No conversation history to compress yet.", show_alert=True)
return
await query.answer("🗜️ Compressing context...")
compress_prompt = (
"Please compress and summarize our entire conversation context and session history into a concise structured memory summary. "
"Retain all critical facts, technical decisions, modified files, active tasks, and code changes, "
"while pruning redundant conversation history to optimize context length."
)
await process_agent_turn(update, context, compress_prompt)
# --- Project Callbacks ---
elif data == "proj_menu":
text, markup = build_projects_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("proj_switch:"):
target_name = data.split(":", 1)[1]
proj = await session_manager.switch_project(chat_id, target_name)
if proj:
await query.answer(f"Switched to: {proj.name}")
else:
await query.answer("Project not found", show_alert=True)
text, markup = build_projects_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
pass
elif data == "proj_new":
if is_fa:
msg = (
"️ <b>ساخت پروژه جدید:</b>\n\n"
"دستور ساخت پروژه را در چت ارسال کنید:\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b>\n"
"• <code>/newproject my-project</code>\n"
"• <code>/newproject webapp</code>"
)
else:
msg = (
"️ <b>Create New Project:</b>\n\n"
"Send command in chat:\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Example:</b>\n"
"• <code>/newproject my-project</code>\n"
"• <code>/newproject webapp</code>"
)
keyboard = [
[
InlineKeyboardButton("📁 پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
]
]
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "proj_share_menu":
text, markup = build_sharing_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("proj_unshare_confirm:"):
target_uid_str = data.split(":", 1)[1]
if target_uid_str.isdigit():
success, msg, _ = await session_manager.unshare_project(chat_id, int(target_uid_str))
await query.answer(strip_ansi(msg)[:100])
text, markup = build_sharing_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
pass
elif data == "proj_del_menu":
own_projs = session_manager.get_user_projects(chat_id)
if not own_projs:
await query.answer("❌ شما پروژه‌ای برای حذف ندارید.", show_alert=True)
return
keyboard = []
for name in own_projs.keys():
lbl = f"🗑️ حذف {name}" if is_fa else f"🗑️ Delete {name}"
keyboard.append([InlineKeyboardButton(lbl, callback_data=f"proj_del_confirm:{name}")])
keyboard.append([InlineKeyboardButton("🔙 بازگشت" if is_fa else "🔙 Back", callback_data="proj_menu")])
title = "🗑️ <b>پروژه‌ای که می‌خواهید حذف شود را انتخاب کنید:</b>" if is_fa else "🗑️ <b>Select project to delete:</b>"
await query.edit_message_text(title, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("proj_del_confirm:"):
target_name = data.split(":", 1)[1]
text = (
f"⚠️ <b>تأیید حذف کامل و هوشمند پروژه:</b> <code>{escape_html(target_name)}</code>\n\n"
f"آیا از حذف کامل این پروژه و آزادسازی کلیه منابع آن اطمینان دارید؟\n\n"
f"<b>منابعی که بررسی و پاکسازی می‌شوند:</b>\n"
f"• 📁 کلیه فایل‌ها و دایرکتوری‌های پروژه در سرور\n"
f"• 🐙 مخزن اختصاصی در سرور گیت (Gitea)\n"
f"• 🌐 ساب‌دامین‌ها و رورس‌پراکسی‌های متصل در Caddy\n"
f"• ⏰ تسک‌ها و کران‌جاب‌های زمان‌بندی‌شده مربوط به این پروژه\n"
f"• 🛑 فرآیندها، پورت‌ها یا سرویس‌های پس‌زمینه فعال"
if is_fa else
f"⚠️ <b>Confirm Full Teardown & Deletion:</b> <code>{escape_html(target_name)}</code>\n\n"
f"Are you sure you want to permanently delete this project and release all its resources?\n\n"
f"<b>Resources to be cleaned up:</b>\n"
f"• 📁 Workspace files and project directory\n"
f"• 🐙 Dedicated repository on Gitea server\n"
f"• 🌐 Subdomains & Caddy reverse proxies\n"
f"• ⏰ Scheduled tasks and cron jobs for this project\n"
f"• 🛑 Running processes, ports and services"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، حذف و پاکسازی کامل" if is_fa else "💥 Yes, Full Teardown", callback_data=f"proj_del_execute:{target_name}"),
InlineKeyboardButton("❌ خیر، انصراف" if is_fa else "❌ No, Cancel", callback_data="proj_menu"),
]
]
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("proj_del_execute:"):
target_name = data.split(":", 1)[1]
await query.answer("⏳ در حال پاکسازی و آزادسازی کامل منابع..." if is_fa else "⏳ Releasing project resources...")
success, msg = await session_manager.delete_project(chat_id, target_name, delete_files=True)
keyboard = [
[InlineKeyboardButton("📁 مدیریت پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu")],
[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")],
]
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif data == "proj_ws_info":
curr_proj = session_manager.get_current_project(chat_id)
if not curr_proj:
await query.answer("پروژه‌ای فعال نیست.", show_alert=True)
return
if is_fa:
msg = (
f"📂 <b>مسیر کاری پروژه {escape_html(curr_proj.name)}:</b>\n"
f"<code>{curr_proj.workspace}</code>\n\n"
f"<i>برای تغییر مسیر این پروژه دستور زیر را ارسال کنید:</i>\n"
f"<code>/workspace /path/to/project</code>"
)
else:
msg = (
f"📂 <b>Workspace for {escape_html(curr_proj.name)}:</b>\n"
f"<code>{curr_proj.workspace}</code>\n\n"
f"<i>To change workspace, send:</i>\n"
f"<code>/workspace /path/to/project</code>"
)
keyboard = [
[
InlineKeyboardButton("📁 پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
]
]
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("proj_upload_link:"):
target_name = data.split(":", 1)[1]
await query.answer("🌐 دریافت لینک اختصاصی آپلود..." if is_fa else "🌐 Generating upload link...")
accessible = session_manager.get_all_accessible_projects(chat_id)
proj = accessible.get(target_name)
if not proj:
for k, p in accessible.items():
if p.name == target_name:
proj = p
break
if not proj:
proj = session_manager.get_current_project(chat_id)
p_name = proj.name if proj else target_name
token = create_upload_token(chat_id, p_name)
upload_url = get_upload_url(token)
if is_fa:
msg = (
f"📤 <b>پنل اختصاصی آپلود فایل در پروژه <code>{escape_html(p_name)}</code></b>\n\n"
f"از طریق صفحه وب زیر می‌توانید هرگونه فایل، پروژه، سورس کد یا فایل‌های فشرده (ZIP) حجیم (بدون محدودیت ۲۰ مگابایت تلگرام تا سقف ۲ گیگابایت) را مستقیماً در مسیر پروژه آپلود کنید:\n\n"
f"🔗 <b>لینک اختصاصی و امن آپلود:</b>\n"
f"{upload_url}\n\n"
f"💡 <i>امکانات صفحه آپلود:</i>\n"
f"• آپلود فایل‌های حجیم با نمایش درصد پیشرفت (Progress Bar)\n"
f"• قابلیت اکسترکت (Unzip) خودکار آرشیوها در پوشه اصلی پروژه\n"
f"• امکان درج یادداشت/پرامپت برای هوش مصنوعی به همراه فایل"
)
keyboard = [
[InlineKeyboardButton("🌐 باز کردن صفحه آپلود", url=upload_url)],
[InlineKeyboardButton("📁 بازگشت به پروژه‌ها", callback_data="proj_menu")],
]
else:
msg = (
f"📤 <b>Web File Upload for Project <code>{escape_html(p_name)}</code></b>\n\n"
f"Upload large files or ZIP archives (up to 2GB) directly to your project workspace:\n\n"
f"🔗 <b>Upload Link:</b>\n"
f"{upload_url}"
)
keyboard = [
[InlineKeyboardButton("🌐 Open Upload Page", url=upload_url)],
[InlineKeyboardButton("📁 Back to Projects", callback_data="proj_menu")],
]
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
elif data.startswith("proj_backup:"):
target_name = data.split(":", 1)[1]
accessible = session_manager.get_all_accessible_projects(chat_id)
proj = accessible.get(target_name)
if not proj:
for k, p in accessible.items():
if p.name == target_name:
proj = p
break
if not proj:
await query.answer("❌ پروژه یافت نشد." if is_fa else "❌ Project not found.", show_alert=True)
return
await query.answer("⏳ در حال آماده‌سازی بکاپ..." if is_fa else "⏳ Preparing backup...")
status_msg = await query.message.reply_html(
f"⏳ <i>در حال آماده‌سازی و فشرده‌سازی بکاپ پروژه <code>{escape_html(proj.name)}</code>...</i>"
if is_fa else
f"⏳ <i>Preparing and compressing backup for project <code>{escape_html(proj.name)}</code>...</i>"
)
await handle_project_backup_flow(
application=context.application,
chat_id=chat_id,
proj_name=proj.name,
workspace=proj.workspace,
is_fa=is_fa,
status_msg=status_msg,
)
elif data == "proj_model_menu":
curr_proj = session_manager.get_current_project(chat_id)
if not curr_proj:
await query.answer("پروژه‌ای فعال نیست.", show_alert=True)
return
keyboard = []
for model_id, label in AVAILABLE_MODELS.items():
is_selected = "✅ " if model_id == curr_proj.model else ""
keyboard.append([InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_model:{model_id}")])
keyboard.append([
InlineKeyboardButton("📁 پروژه‌ها" if is_fa else "📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
])
await query.edit_message_text(
f"🧠 <b>Select AI Model</b>\nProject: <code>{escape_html(curr_proj.name)}</code>\nCurrent: <code>{curr_proj.model}</code>",
parse_mode=constants.ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
elif data == "btn_memory_menu" or data.startswith("mem_tab:"):
parts = data.split(":")
v_type = parts[1] if len(parts) > 1 else "project"
page_num = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 0
if v_type == "global" and not is_admin_user:
await query.answer("🚫 مشاهده حافظه عمومی فقط برای مدیر سیستم مجاز است." if is_fa else "🚫 Global memory access restricted to administrators.", show_alert=True)
v_type = "project" if session_manager.get_current_project(chat_id) else "user"
page_num = 0
text, markup = build_memory_menu(chat_id, view_type=v_type, page=page_num)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("mem_del_conf:"):
_, m_id_str, v_type, page_str = data.split(":")
m_id = int(m_id_str)
p_num = int(page_str)
from memory_manager import memory_manager
mem = memory_manager.get_by_id(m_id)
if not mem:
await query.answer("❌ خاطره یافت نشد." if is_fa else "❌ Memory not found.", show_alert=True)
text, markup = build_memory_menu(chat_id, view_type=v_type, page=p_num)
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
elif mem.is_global and not is_admin_user:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
return
else:
confirm_text = (
f"🗑️ <b>آیا از حذف این خاطره اطمینان دارید؟</b>\n\n"
f"• 🔑 <b>کلید:</b> <code>{escape_html(mem.key)}</code>\n"
f"• 📝 <b>محتوا:</b> {escape_html(mem.content)}"
if is_fa
else
f"🗑️ <b>Are you sure you want to delete this memory?</b>\n\n"
f"• 🔑 <b>Key:</b> <code>{escape_html(mem.key)}</code>\n"
f"• 📝 <b>Content:</b> {escape_html(mem.content)}"
)
keyboard = [
[
InlineKeyboardButton("✅ بله، حذف شود" if is_fa else "✅ Yes, Delete", callback_data=f"mem_del_do:{m_id}:{v_type}:{p_num}"),
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data=f"mem_tab:{v_type}:{p_num}"),
]
]
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("mem_del_do:"):
_, m_id_str, v_type, page_str = data.split(":")
m_id = int(m_id_str)
p_num = int(page_str)
from memory_manager import memory_manager
mem = memory_manager.get_by_id(m_id)
if mem and mem.is_global and not is_admin_user:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
return
try:
ok = memory_manager.delete_by_id(m_id, user_id=chat_id, is_admin=is_admin_user)
if ok:
await query.answer("🗑️ خاطره با موفقیت حذف شد." if is_fa else "🗑️ Memory deleted successfully.")
else:
await query.answer("❌ خاطره یافت نشد." if is_fa else "❌ Memory not found.", show_alert=True)
except PermissionError:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
except Exception as e:
await query.answer(f"⚠️ خطا: {e}", show_alert=True)
text, markup = build_memory_menu(chat_id, view_type=v_type, page=p_num)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("mem_clear_conf:"):
v_type = data.split(":")[1]
if v_type == "global" and not is_admin_user:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
return
type_label = "عمومی سیستم" if v_type == "global" else "اختصاصی خودتان"
confirm_text = (
f"⚠️ <b>هشدار پاک‌سازی حافظه</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید تمام خاطرات <b>{type_label}</b> را به صورت کامل پاک کنید؟ این عملیات غیرقابل بازگشت است."
if is_fa
else
f"⚠️ <b>Warning: Clear Memory</b>\n\n"
f"Are you sure you want to permanently clear all <b>{v_type}</b> memories?"
)
keyboard = [
[
InlineKeyboardButton("💥 بله، همه را پاک کن" if is_fa else "💥 Yes, Clear All", callback_data=f"mem_clear_do:{v_type}"),
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data=f"mem_tab:{v_type}:0"),
]
]
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("mem_clear_do:"):
v_type = data.split(":")[1]
if v_type == "global" and not is_admin_user:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
return
from memory_manager import memory_manager
curr_p = session_manager.get_current_project(chat_id)
p_name = curr_p.name if curr_p else "default"
try:
cnt = memory_manager.clear_memories(
type_=v_type,
user_id=chat_id,
project_name=p_name if v_type == "project" else None,
is_admin=is_admin_user,
)
await query.answer(f"🧹 تعداد {cnt} خاطره پاک شد." if is_fa else f"🧹 Cleared {cnt} memories.")
except PermissionError:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
except Exception as e:
await query.answer(f"⚠️ خطا: {e}", show_alert=True)
text, markup = build_memory_menu(chat_id, view_type=v_type, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("mem_add_menu:") or data.startswith("mem_add_tab:"):
parts = data.split(":")
v_type = parts[1] if len(parts) > 1 else "project"
if v_type == "global" and not is_admin_user:
await query.answer("🚫 افزودن حافظه عمومی فقط مختص مدیر سیستم است." if is_fa else "🚫 Global memory restricted to administrators.", show_alert=True)
v_type = "project" if session_manager.get_current_project(chat_id) else "user"
text, markup = build_memory_add_menu(chat_id, view_type=v_type)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("mem_add_preset:"):
_, target_type, preset_name = data.split(":", 2)
if target_type == "global" and not is_admin_user:
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
return
from memory_manager import memory_manager
curr_p = session_manager.get_current_project(chat_id)
p_name = curr_p.name if curr_p else "default"
preset_data = MEMORY_PRESETS.get(target_type, {}).get(preset_name)
if not preset_data:
await query.answer("❌ الگوی مورد نظر یافت نشد." if is_fa else "❌ Preset not found.", show_alert=True)
return
try:
item, is_created = memory_manager.save_or_update(
type_=target_type,
key=preset_data["key"],
content=preset_data["content"] if is_fa else preset_data.get("content_en", preset_data["content"]),
user_id=chat_id,
project_name=p_name if target_type == "project" else None,
category=preset_data.get("category", "general"),
importance=5 if target_type == "global" else 3,
created_by=chat_id,
)
action_str = "ثبت شد" if is_created else "به‌روزرسانی شد"
await query.answer(f"✅ خاطره «{item.key}» با موفقیت {action_str}!" if is_fa else f"✅ Memory '{item.key}' saved!", show_alert=True)
except Exception as e:
await query.answer(f"⚠️ خطا در ثبت خاطره: {e}", show_alert=True)
text, markup = build_memory_menu(chat_id, view_type=target_type, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_tasks_menu":
text, markup = build_tasks_menu(chat_id, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("tasks_page:"):
page_num = int(data.split(":", 1)[1])
text, markup = build_tasks_menu(chat_id, page=page_num)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("task_detail:"):
task_id = data.split(":", 1)[1]
text, markup = build_task_detail_menu(chat_id, task_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("task_run:"):
task_id = data.split(":", 1)[1]
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
return
await query.answer("⏳ در حال اجرای تسک..." if is_fa else "⏳ Executing task...")
status_msg = await query.message.reply_html(
f"⏳ <b>در حال اجرای تسک <code>{task_id}</code>...</b>" if is_fa else f"⏳ <b>Executing task <code>{task_id}</code>...</b>"
)
success, out = await task_scheduler.execute_task_now(task_id, context.application)
try:
await status_msg.delete()
except Exception:
pass
elif data.startswith("task_pause:"):
task_id = data.split(":", 1)[1]
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
return
ok = task_scheduler.pause_task(task_id)
if ok:
await query.answer("⏸️ تسک متوقف شد." if is_fa else "⏸️ Task paused.")
text, markup = build_task_detail_menu(chat_id, task_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
else:
await query.answer("خطا در توقف تسک." if is_fa else "Failed to pause task.", show_alert=True)
elif data.startswith("task_resume:"):
task_id = data.split(":", 1)[1]
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
return
ok = task_scheduler.resume_task(task_id)
if ok:
await query.answer("▶️ تسک فعال شد." if is_fa else "▶️ Task resumed.")
text, markup = build_task_detail_menu(chat_id, task_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
else:
await query.answer("خطا در فعال‌سازی تسک." if is_fa else "Failed to resume task.", show_alert=True)
elif data.startswith("task_del:"):
task_id = data.split(":", 1)[1]
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
return
ok = task_scheduler.delete_task(task_id)
if ok:
await query.answer("🗑️ تسک با موفقیت حذف شد." if is_fa else "🗑️ Task deleted.")
text, markup = build_tasks_menu(chat_id, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
else:
await query.answer("❌ تسک یافت نشد." if is_fa else "❌ Task not found.", show_alert=True)
elif data.startswith("task_last_out:"):
task_id = data.split(":", 1)[1]
task = task_scheduler.get_task(task_id)
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
return
if not task.last_run_result:
await query.answer("❌ خروجی ثبت نشده است." if is_fa else "❌ No output recorded.", show_alert=True)
return
await query.answer()
header = f"📜 <b>آخرین خروجی ثبت شده تسک <code>{task.id}</code>:</b>\n\n" if is_fa else f"📜 <b>Last Recorded Output for Task <code>{task.id}</code>:</b>\n\n"
if task.task_type == "prompt":
formatted = header + markdown_to_telegram_html(task.last_run_result)
else:
formatted = header + f"<pre>{escape_html(task.last_run_result)}</pre>"
chunks = split_message(formatted, max_length=settings.max_message_length)
for chunk in chunks:
await query.message.reply_html(chunk, disable_web_page_preview=True)
elif data == "task_clear_completed":
cnt = task_scheduler.clear_completed_tasks(chat_id)
await query.answer(f"🧹 {cnt} تسک پایان‌یافته پاکسازی شد." if is_fa else f"🧹 {cnt} completed tasks cleared.")
text, markup = build_tasks_menu(chat_id, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "task_new_guide":
text, markup = build_task_add_guide(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "req_access":
user = update.effective_user
if not user:
return
uid = user.id
if settings.is_user_authorized(uid):
await query.answer("✅ شما از قبل مجاز هستید." if is_fa else "✅ You are already authorized.", show_alert=True)
return
req = invite_manager.add_pending_request(
user_id=uid,
username=user.username or "",
first_name=user.first_name or "",
last_name=user.last_name or "",
)
await query.answer("✅ درخواست شما با موفقیت برای مدیران ارسال شد." if is_fa else "✅ Request sent to administrators.", show_alert=True)
if is_fa:
user_confirm = (
f"📩 <b>درخواست دسترسی شما ثبت شد!</b>\n\n"
f"اطلاعات شما (شناسه: <code>{uid}</code>) برای مدیران سیستم ارسال گردید.\n"
f"به محض تایید توسط مدیر، از طریق همین بات به شما اطلاع داده خواهد شد."
)
else:
user_confirm = (
f"📩 <b>Access Request Submitted!</b>\n\n"
f"Your request (User ID: <code>{uid}</code>) has been forwarded to administrators.\n"
f"You will be notified here as soon as your access is approved."
)
try:
await query.edit_message_text(user_confirm, parse_mode=constants.ParseMode.HTML)
except Exception:
pass
for admin_id in settings.admin_user_ids:
try:
admin_alert = (
f"🔔 <b>درخواست دسترسی کاربر جدید به ربات:</b>\n\n"
f"• 👤 <b>نام کاربر:</b> {escape_html(req.full_name)}\n"
f"• 🆔 <b>شناسه (User ID):</b> <code>{req.user_id}</code>\n"
f"• 🏷️ <b>نام کاربری:</b> @{escape_html(req.username) if req.username else '(ندارد)'}\n"
f"• 🕒 <b>زمان درخواست:</b> {format_timestamp(req.requested_at, is_fa=True)}\n"
)
adm_kb = [
[
InlineKeyboardButton("✅ تایید و صدور دسترسی", callback_data=f"req_approve:{req.user_id}"),
InlineKeyboardButton("❌ رد درخواست", callback_data=f"req_deny:{req.user_id}"),
]
]
await context.application.bot.send_message(
chat_id=admin_id,
text=admin_alert,
parse_mode=constants.ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(adm_kb),
)
except Exception as e:
logger.error(f"Failed to send access request alert to admin {admin_id}: {e}")
elif data.startswith("req_approve:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
target_uid = int(data.split(":", 1)[1])
invite_manager.approve_request(target_uid)
await query.answer("✅ دسترسی کاربر تایید شد." if is_fa else "✅ User approved.")
try:
await query.edit_message_text(
f"✅ <b>دسترسی کاربر <code>{target_uid}</code> با موفقیت تایید و فعال شد.</b>",
parse_mode=constants.ParseMode.HTML,
)
except Exception:
pass
try:
user_notif = (
f"🎉 <b>تبریک! دسترسی شما به ربات تایید شد.</b>\n\n"
f"مدیر سیستم دسترسی شما را فعال کرد.\n"
f"برای ورود به کنترل‌پنل و شروع گفتگو روی /start بزنید."
)
await context.application.bot.send_message(
chat_id=target_uid,
text=user_notif,
parse_mode=constants.ParseMode.HTML,
)
except Exception:
pass
elif data.startswith("req_deny:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
target_uid = int(data.split(":", 1)[1])
invite_manager.deny_request(target_uid)
await query.answer("❌ درخواست کاربر رد شد." if is_fa else "❌ Request denied.")
try:
await query.edit_message_text(
f"❌ <b>درخواست دسترسی کاربر <code>{target_uid}</code> رد شد.</b>",
parse_mode=constants.ParseMode.HTML,
)
except Exception:
pass
elif data == "btn_users_menu":
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
text, markup = build_users_menu(chat_id, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("users_page:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
page_num = int(data.split(":", 1)[1])
text, markup = build_users_menu(chat_id, page=page_num)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_invites_menu":
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
text, markup = build_invites_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_pending_reqs":
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
text, markup = build_pending_requests_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_gen_invite":
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
bot_username = (await context.application.bot.get_me()).username or "AGYBot"
token = invite_manager.create_invite(creator_id=user_id, max_uses=1, duration_hours=None)
link = f"https://t.me/{bot_username}?start={token.code}"
if is_fa:
msg = (
f"🔗 <b>لینک دعوت یک‌بار مصرف جدید:</b>\n\n"
f"• 📋 <b>لینک:</b>\n<code>{link}</code>\n\n"
f"• 🔑 <b>کد:</b> <code>{token.code}</code>\n"
f"• 👥 <b>ظرفیت:</b> <code>۱ کاربر (یک‌بار مصرف)</code>\n"
f"• ⏳ <b>اعتبار:</b> <code>دائمی تا اولین استفاده</code>\n\n"
f"💡 <i>این لینک را برای کاربر مورد نظر ارسال کنید.</i>"
)
else:
msg = (
f"🔗 <b>Single-Use Invite Link Generated:</b>\n\n"
f"• 📋 <b>Link:</b>\n<code>{link}</code>\n\n"
f"• 🔑 <b>Code:</b> <code>{token.code}</code>\n"
f"• 👥 <b>Capacity:</b> <code>1 User (Single-Use)</code>\n\n"
f"💡 <i>Share this link with the invited user.</i>"
)
keyboard = [
[InlineKeyboardButton("📋 مشاهده لینک‌های فعال" if is_fa else "📋 Active Links", callback_data="btn_invites_menu")],
[InlineKeyboardButton("👥 مدیریت کاربران" if is_fa else "👥 User Manager", callback_data="btn_users_menu")],
]
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("user_revoke_ask:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
target_uid = int(data.split(":", 1)[1])
if is_fa:
msg = (
f"⚠️ <b>تأیید لغو دسترسی:</b>\n\n"
f"آیا مطمئن هستید که می‌خواهید دسترسی کاربر <code>{target_uid}</code> به ربات را لغو کنید؟"
)
keyboard = [
[
InlineKeyboardButton("🚫 بله، لغو دسترسی", callback_data=f"user_revoke_confirm:{target_uid}"),
InlineKeyboardButton("❌ انصراف", callback_data="btn_users_menu"),
]
]
else:
msg = (
f"⚠️ <b>Confirm Revoke Access:</b>\n\n"
f"Are you sure you want to revoke access for user <code>{target_uid}</code>?"
)
keyboard = [
[
InlineKeyboardButton("🚫 Yes, Revoke Access", callback_data=f"user_revoke_confirm:{target_uid}"),
InlineKeyboardButton("❌ Cancel", callback_data="btn_users_menu"),
]
]
try:
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
except Exception:
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
elif data.startswith("user_revoke_confirm:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
target_uid = int(data.split(":", 1)[1])
settings.remove_authorized_user(target_uid)
session_manager.cancel_active_task(target_uid)
await query.answer("🚫 دسترسی کاربر با موفقیت لغو شد." if is_fa else "🚫 User access revoked.")
text, markup = build_users_menu(chat_id, page=0)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data.startswith("invite_revoke:"):
if not settings.is_admin(user_id):
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
return
code_id = data.split(":", 1)[1]
invite_manager.revoke_invite(code_id)
await query.answer("🗑️ لینک دعوت باطل شد." if is_fa else "🗑️ Invite link revoked.")
text, markup = build_invites_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "proj_close":
try:
await query.message.delete()
except Exception:
pass
try:
await query.answer()
except Exception:
pass
# Main Message & Prompt Processing Pipeline
@check_auth
async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message or not update.message.text:
return
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
# 0. Check if user has an active OAuth login waiting for authorization code
if chat_id in auth_manager.active_logins:
code_input = update.message.text.strip()
if code_input.lower() in ("/cancel", "لغو", "انصراف", "cancel"):
auth_manager.cancel_oauth_login(chat_id)
text, markup = build_account_menu(chat_id)
await update.message.reply_html("❌ فرآیند لاگین گوگل لغو گردید.\n\n" + text, reply_markup=markup)
return
wait_msg = await update.message.reply_html("⏳ <i>در حال ثبت و فعال‌سازی حساب کاربری با کد دریافتی...</i>" if is_fa else "⏳ <i>Completing login with authorization code...</i>")
success, msg = await auth_manager.complete_oauth_login(chat_id, code_input)
if success:
text, markup = build_account_menu(chat_id)
await wait_msg.edit_text(f"{msg}\n\n{text}", parse_mode=constants.ParseMode.HTML, reply_markup=markup)
else:
await wait_msg.edit_text(msg, parse_mode=constants.ParseMode.HTML)
return
# Check if user sent a JSON token directly in text
raw_stripped = update.message.text.strip()
if raw_stripped.startswith("{") and raw_stripped.endswith("}") and ("token" in raw_stripped or "access_token" in raw_stripped):
success, msg = auth_manager.import_token(chat_id, raw_stripped)
if success:
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
# 1. Check if user is in an interactive input state (e.g. configuring FTP fields)
if chat_id in USER_INPUT_STATES:
state = USER_INPUT_STATES.pop(chat_id)
if state.get("type") == "ftp_set" and curr_proj:
field = state.get("field")
val = update.message.text.strip()
if field == "host":
# Check for host:port format
if ":" in val:
h, p = val.rsplit(":", 1)
curr_proj.ftp_host = h.strip()
try:
curr_proj.ftp_port = int(p.strip())
except Exception:
pass
else:
curr_proj.ftp_host = val
elif field == "user":
curr_proj.ftp_user = val
elif field == "pass":
curr_proj.ftp_password = val
# Delete password message from chat if possible for security
try:
await update.message.delete()
except Exception:
pass
elif field == "path":
curr_proj.ftp_path = val if val.startswith("/") else f"/{val}"
session_manager.save()
field_labels = {
"host": "هاست و پورت",
"user": "نام کاربری",
"pass": "رمز عبور",
"path": "مسیر مقصد",
}
label_fa = field_labels.get(field, field)
ack_msg = f"✅ <b>{label_fa} با موفقیت ذخیره شد.</b>" if is_fa else f"✅ <b>{field} updated successfully.</b>"
await update.message.reply_html(ack_msg)
# Auto-test connection if host, user and password are all present
if curr_proj.ftp_host and curr_proj.ftp_user and curr_proj.ftp_password:
test_ok, test_msg = await ftp_manager.test_connection(
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user or "",
password=curr_proj.ftp_password or "",
path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
)
test_status = "✅ اتصال FTP با موفقیت برقرار و تایید شد." if test_ok else f"⚠️ هشدار در تست اتصال: {test_msg}"
await update.message.reply_html(f"<i>{test_status}</i>")
text, markup = await build_ftp_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
return
if state.get("type") == "ssh_set" and curr_proj:
field = state.get("field")
val = update.message.text.strip()
if field == "host":
if ":" in val:
h, p = val.rsplit(":", 1)
curr_proj.ssh_host = h.strip()
try:
curr_proj.ssh_port = int(p.strip())
except Exception:
curr_proj.ssh_port = 22
else:
curr_proj.ssh_host = val
curr_proj.ssh_port = 22
elif field == "user":
curr_proj.ssh_user = val
elif field == "pass":
curr_proj.ssh_password = val
try:
await update.message.delete()
except Exception:
pass
elif field == "key":
curr_proj.ssh_key = val
try:
await update.message.delete()
except Exception:
pass
elif field == "path":
curr_proj.ssh_path = val if val.startswith("/") else f"/{val}"
session_manager.save()
field_labels = {
"host": "هاست و پورت SSH",
"user": "نام کاربری SSH",
"pass": "رمز عبور SSH",
"key": "کلید اختصاصی SSH Key",
"path": "مسیر مقصد ریموت",
}
label_fa = field_labels.get(field, field)
ack_msg = f"✅ <b>{label_fa} با موفقیت ذخیره شد.</b>" if is_fa else f"✅ <b>SSH {field} updated successfully.</b>"
await update.message.reply_html(ack_msg)
# Auto-test connection if host is present
if curr_proj.ssh_host:
test_ok, test_msg = await ssh_manager.test_connection(
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user or "",
password=curr_proj.ssh_password,
key=curr_proj.ssh_key,
path=curr_proj.ssh_path or "/",
user_id=chat_id,
project_name=curr_proj.name,
requester="user_button",
)
test_status = "✅ اتصال SSH با موفقیت بررسی و تایید شد." if test_ok else f"⚠️ نتیجه تست اتصال SSH: {test_msg}"
await update.message.reply_html(f"<i>{test_status}</i>")
text, markup = await build_ssh_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
return
if state.get("type") == "ssh_exec" and curr_proj:
cmd = update.message.text.strip()
if not curr_proj.ssh_host:
await update.message.reply_html("⚠️ اطلاعات سرور SSH تنظیم نشده است.")
text, markup = await build_ssh_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
return
wait_msg = await update.message.reply_html(f"⏳ <i>در حال اجرای فرمان در سرور ریموت...</i>\n<code>{escape_html(cmd)}</code>")
ok, out, code = await ssh_manager.execute_command(
host=curr_proj.ssh_host,
port=curr_proj.ssh_port,
user=curr_proj.ssh_user or "",
password=curr_proj.ssh_password,
key=curr_proj.ssh_key,
remote_path=curr_proj.ssh_path or "/",
command=cmd,
user_id=chat_id,
project_name=curr_proj.name,
requester="user_command",
)
st_icon = "✅" if ok else "❌"
res_text = (
f"{st_icon} <b>نتیجه اجرای دستور ریموت:</b>\n"
f"• 💻 <b>دستور:</b> <code>{escape_html(cmd)}</code>\n"
f"• 🔢 <b>کد خروج:</b> <code>{code}</code>\n"
f"• 📜 <b>خروجی:</b>\n<pre>{escape_html(out)}</pre>"
)
try:
await wait_msg.edit_text(res_text, parse_mode=constants.ParseMode.HTML)
except Exception:
await update.message.reply_html(res_text)
text, markup = await build_ssh_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
return
if not curr_proj:
if is_fa:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"برای ارسال دستورات و تعامل با هوش مصنوعی، لطفاً ابتدا یک پروژه ایجاد کنید:\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b> <code>/newproject my-project</code>"
)
keyboard = [[InlineKeyboardButton("➕ ساخت پروژه جدید", callback_data="proj_new")]]
else:
msg = (
"⚠️ <b>You have not created any projects yet!</b>\n\n"
"To interact with AI, please create a project first:\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Example:</b> <code>/newproject my-project</code>"
)
keyboard = [[InlineKeyboardButton(" Create Project", callback_data="proj_new")]]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
if not auth_manager.has_custom_account(chat_id):
if is_fa:
msg = (
"⚠️ <b>حساب کاربری هوش مصنوعی شما متصل نیست!</b>\n\n"
"در این ربات حساب‌های اشتراکی غیرفعال هستند و هر کاربر ملزم به استفاده از حساب اختصاصی گوگل خود می‌باشد.\n\n"
"👉 لطفاً با دستور <code>/login</code> یا دکمه زیر وارد حساب اختصاصی گوگل خود شوید یا فایل توکن را ارسال فرمایید."
)
keyboard = [
[InlineKeyboardButton("🔑 ورود با حساب گوگل (/login)", callback_data="btn_account_start_login")],
[InlineKeyboardButton("📋 منوی حساب کاربری (/account)", callback_data="btn_account_menu")],
]
else:
msg = (
"⚠️ <b>AGY Account Not Connected!</b>\n\n"
"Shared accounts are disabled. You must connect your personal Google account to interact with the AI.\n\n"
"👉 Send <code>/login</code> or tap the button below."
)
keyboard = [
[InlineKeyboardButton("🔑 Log in with Google (/login)", callback_data="btn_account_start_login")],
[InlineKeyboardButton("📋 Account Menu (/account)", callback_data="btn_account_menu")],
]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
prompt = update.message.text.strip()
await process_agent_turn(update, context, prompt)
@check_auth
async def file_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
message = update.message
chat_id = update.effective_chat.id
curr_proj = session_manager.get_current_project(chat_id)
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
# Check if uploaded document is antigravity-oauth-token or a token json
if message.document:
doc_name = (message.document.file_name or "").lower()
if "oauth-token" in doc_name or doc_name.endswith(".token") or (doc_name.endswith(".json") and "token" in doc_name):
try:
doc_file = await message.document.get_file()
content_bytes = await doc_file.download_as_bytearray()
content_str = content_bytes.decode("utf-8", errors="ignore")
success, msg = auth_manager.import_token(chat_id, content_str)
if success:
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
except Exception as ex:
logger.error(f"Failed to process uploaded token file: {ex}")
if not auth_manager.has_custom_account(chat_id):
msg = (
"⚠️ <b>حساب کاربری هوش مصنوعی شما متصل نیست!</b>\n\n"
"برای ارسال فایل و تعامل با هوش مصنوعی، لطفاً ابتدا با حساب اختصاصی خود وارد شوید (/login)."
if is_fa else
"⚠️ <b>AGY Account Not Connected!</b>\n\nPlease connect your personal Google account first via /login."
)
keyboard = [[InlineKeyboardButton("🔑 ورود به حساب گوگل (/login)" if is_fa else "🔑 Log in with Google", callback_data="btn_account_start_login")]]
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"لطفاً ابتدا با دستور <code>/newproject &lt;نام_پروژه&gt;</code> یک پروژه بسازید."
if is_fa else
"⚠️ <b>You have not created any projects yet!</b>\n\n"
"Please create a project first using <code>/newproject &lt;name&gt;</code>."
)
await update.message.reply_html(msg)
return
file_obj = None
file_name = "attached_file"
is_voice = False
# Pre-check file size before calling Telegram API get_file (Telegram Bot API limit is 20MB = 20,971,520 bytes)
doc_size = None
if message.document and message.document.file_size:
doc_size = message.document.file_size
elif message.audio and message.audio.file_size:
doc_size = message.audio.file_size
if doc_size and doc_size > 20 * 1024 * 1024:
size_mb = f"{doc_size / (1024 * 1024):.1f} MB"
err_msg = (
f"❌ <b>حجم فایل ارسالی ({size_mb}) بیشتر از سقف مجاز تلگرام است!</b>\n\n"
f"⚠️ <b>محدودیت رسمی تلگرام:</b> ربات‌های تلگرام به دلیل محدودیت‌های سرور تلگرام اجازه دانلود فایل‌های بزرگتر از <b>۲۰ مگابایت</b> را ندارند.\n\n"
f"💡 <b>راه‌حل‌ها:</b>\n"
f"۱. فایل‌های غیرضروری (مانند <code>node_modules</code>، <code>.git</code>، <code>venv</code>، کش‌ها، عکس یا ویدیوهای سنگین) را از فایل زیپ حذف و مجدداً فشرده کنید تا زیر ۲۰ مگابایت شود.\n"
f"۲. یا فایل را از طریق SCP/SFTP مستقیم روی سرور قرار دهید و آدرس آن را بفرستید."
if is_fa else
f"❌ <b>Uploaded file ({size_mb}) exceeds Telegram Bot limit (20MB)!</b>\n\n"
f"Please exclude heavy folders (like <code>node_modules</code>, <code>venv</code>, <code>.git</code>) and re-upload."
)
await update.effective_message.reply_html(err_msg)
return
try:
if message.document:
file_obj = await message.document.get_file()
file_name = message.document.file_name or "uploaded_doc"
elif message.photo:
file_obj = await message.photo[-1].get_file()
file_name = f"photo_{int(time.time())}.jpg"
elif message.voice:
file_obj = await message.voice.get_file()
file_name = f"voice_{int(time.time())}.ogg"
is_voice = True
elif message.audio:
file_obj = await message.audio.get_file()
file_name = message.audio.file_name or f"audio_{int(time.time())}.mp3"
is_voice = True
if not file_obj:
return
dest_dir = Path(curr_proj.workspace) / "uploads"
dest_dir.mkdir(parents=True, exist_ok=True)
dest_path = dest_dir / file_name
await file_obj.download_to_drive(custom_path=dest_path)
except BadRequest as be:
if "file is too big" in str(be).lower():
size_mb = f"{message.document.file_size / (1024*1024):.1f} MB" if message.document and message.document.file_size else ">20 MB"
err_msg = (
f"❌ <b>حجم فایل ارسالی ({size_mb}) بیشتر از سقف مجاز تلگرام است!</b>\n\n"
f"⚠️ <b>محدودیت تلگرام:</b> بات‌های استاندارد تلگرام طبق قوانین سرورهای تلگرام اجازه دانلود فایل‌های بزرگتر از <b>۲۰ مگابایت</b> را ندارند.\n\n"
f"💡 <b>راه‌حل‌ها:</b>\n"
f"۱. فایل‌های غیرضروری (مانند <code>node_modules</code>، <code>.git</code>، <code>venv</code>، ویدیوها یا عکس‌های سنگین) را از فایل زیپ حذف و مجدداً فشرده کنید.\n"
f"۲. یا فایل را در سرور قرار دهید و مسیر آن را بفرستید."
if is_fa else
f"❌ <b>Uploaded file ({size_mb}) exceeds Telegram Bot limit (20MB)!</b>\n\n"
f"Please exclude heavy folders (like <code>node_modules</code>, <code>venv</code>, <code>.git</code>) and re-upload."
)
await update.effective_message.reply_html(err_msg)
return
logger.error(f"Download BadRequest error: {be}")
await update.effective_message.reply_html(f"❌ <b>خطا در دریافت فایل:</b> <code>{escape_html(str(be))}</code>")
return
except Exception as down_err:
logger.error(f"Failed to download attached file: {down_err}", exc_info=True)
await update.effective_message.reply_html(f"❌ <b>خطا در دانلود فایل:</b> <code>{escape_html(str(down_err))}</code>")
return
if is_voice:
status_msg = await update.effective_message.reply_html(
"🎙️ <i>در حال گوش دادن به پیام صوتی و تبدیل گفتار به متن...</i>"
if is_fa else
"🎙️ <i>Listening to voice message and transcribing speech...</i>"
)
try:
from voice_transcriber import transcribe_audio_file
ok, text, detected_lang = await transcribe_audio_file(str(dest_path), language=session.language or "fa")
except Exception as e:
logger.error(f"Voice transcription error: {e}")
ok, text = False, ""
if ok and text and text.strip():
# Delete audio file immediately after successful transcription
try:
if dest_path.exists():
dest_path.unlink()
except Exception as e:
logger.warning(f"Failed to delete temp voice file {dest_path}: {e}")
caption = message.caption or ""
transcribed_badge = (
f"🎙️ <b>متن پیام صوتی شما:</b>\n«<code>{escape_html(text.strip())}</code>»\n\n"
if is_fa else
f"🎙️ <b>Voice Transcription:</b>\n«<code>{escape_html(text.strip())}</code>»\n\n"
)
full_prompt = f"[User sent a voice message transcribed as: \"{text.strip()}\"]\n\n{text.strip()}"
if caption:
full_prompt += f"\nCaption: {caption}"
try:
await status_msg.edit_text(
transcribed_badge + ("💭 <i>در حال پردازش و اجرای دستور...</i>" if is_fa else "💭 <i>Processing request...</i>"),
parse_mode=constants.ParseMode.HTML,
)
except Exception:
pass
await process_agent_turn(update, context, full_prompt, status_msg_to_reuse=status_msg)
return
else:
caption = message.caption or "Please listen to and process the instructions in this voice message."
full_prompt = f"[User uploaded voice audio file to `{dest_path}`]\n\n{caption}"
await process_agent_turn(update, context, full_prompt, status_msg_to_reuse=status_msg, temp_file_to_cleanup=str(dest_path))
return
caption = message.caption or ""
is_zip = file_name.lower().endswith((".zip", ".tar.gz", ".tgz", ".tar", ".gz", ".bz2", ".7z", ".rar"))
if is_zip:
# Check if it is a standard zip archive that can be extracted or inspected
extracted_info = ""
if file_name.lower().endswith(".zip"):
try:
import zipfile
with zipfile.ZipFile(dest_path, "r") as zf:
namelist = zf.namelist()
total_files = len(namelist)
sample_files = namelist[:15]
sample_str = "\n".join([f" - {f}" for f in sample_files])
if total_files > 15:
sample_str += f"\n ... and {total_files - 15} more files"
extracted_info = f"\nArchive contains {total_files} files/directories:\n{sample_str}"
except Exception as ze:
logger.warning(f"Could not inspect zip file contents: {ze}")
prompt_caption = f"\nUser caption/instruction: {caption}" if caption else ""
full_prompt = (
f"[User uploaded a ZIP/archive file `{file_name}` to project workspace uploads: `{dest_path}`]"
f"{extracted_info}"
f"{prompt_caption}\n\n"
f"The archive file has been saved to `{dest_path}` in the project uploads folder. "
f"Project workspace root is `{curr_proj.workspace}`. "
f"Please review the uploaded archive, unpack/extract it to `{curr_proj.workspace}` if appropriate or requested, inspect its files, and proceed with the user request."
)
await process_agent_turn(update, context, full_prompt)
return
full_caption = f"\nCaption: {caption}" if caption else ""
full_prompt = f"[User uploaded file to `{dest_path}`]{full_caption}"
await process_agent_turn(update, context, full_prompt)
async def process_agent_turn(
update: Update,
context: ContextTypes.DEFAULT_TYPE,
prompt: str,
status_msg_to_reuse: Optional[Any] = None,
temp_file_to_cleanup: Optional[str] = None,
):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
"برای ارسال درخواست و گفتگو با هوش مصنوعی، لطفاً ابتدا یک پروژه بسازید:\n"
"<code>/newproject &lt;نام_پروژه&gt;</code>\n\n"
"<b>مثال:</b> <code>/newproject myapp</code>"
if is_fa else
"⚠️ <b>You have not created any projects yet!</b>\n\n"
"Please create a project first:\n"
"<code>/newproject &lt;project_name&gt;</code>\n\n"
"<b>Example:</b> <code>/newproject myapp</code>"
)
keyboard = [[InlineKeyboardButton("➕ ساخت پروژه جدید", callback_data="proj_new")]]
await update.effective_message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
if not auth_manager.has_custom_account(chat_id):
msg = (
"⚠️ <b>حساب کاربری هوش مصنوعی شما متصل نیست!</b>\n\n"
"در این ربات حساب‌های عمومی و اشتراکی غیرفعال هستند. برای استفاده از هوش مصنوعی، لطفاً با دستور <code>/login</code> یا دکمه زیر وارد حساب اختصاصی گوگل خود شوید."
if is_fa else
"⚠️ <b>AGY Account Not Connected!</b>\n\nShared accounts are disabled. Please connect your personal Google account via /login."
)
keyboard = [[InlineKeyboardButton("🔑 ورود به حساب گوگل (/login)" if is_fa else "🔑 Log in with Google", callback_data="btn_account_start_login")]]
if status_msg_to_reuse:
await status_msg_to_reuse.edit_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
else:
await update.effective_message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
return
session_manager.cancel_active_task(chat_id)
model_to_use = getattr(curr_proj, "model", None) or getattr(session, "model", None) or settings.default_model or "gemini-3.7-flash-auto"
effort_to_use = getattr(curr_proj, "reasoning_effort", None) or getattr(session, "reasoning_effort", None)
model_display = get_model_display_name(model_to_use, effort_to_use)
# Initial thinking status message clearly stating which project and model are active
if status_msg_to_reuse:
status_msg = status_msg_to_reuse
else:
curr_cid = getattr(curr_proj, "conversation_id", None)
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
proj_label = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
status_lines = [f"🤔 <b>{escape_html(model_display)}</b>"]
if conv_title:
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
initial_status = f"{proj_label}\n\n" + "\n".join(status_lines)
status_msg = await update.effective_message.reply_html(
initial_status,
reply_markup=get_stop_button(session.language),
)
# Mark turn in progress for recovery upon crash/restart
session.turn_in_progress = True
session.last_prompt = prompt
session.last_status_msg_id = status_msg.message_id
session.last_delivered = False
session.last_response = ""
session.last_update_time = time.time()
session_manager.save()
accumulated_tokens = []
thoughts = []
tools = []
last_edit_time = 0.0
edit_lock = asyncio.Lock()
is_cancelled = False
async def update_telegram_display(final: bool = False):
nonlocal last_edit_time
if is_cancelled:
return
now = time.time()
if not final and (now - last_edit_time < settings.stream_edit_interval):
return
async with edit_lock:
if is_cancelled:
return
if not final and (time.time() - last_edit_time < settings.stream_edit_interval):
return
last_edit_time = time.time()
raw_text = "".join(accumulated_tokens)
session.last_response = raw_text
curr_proj.last_response = raw_text
# Get dynamic conversation topic if set or updated
curr_cid = getattr(curr_proj, "conversation_id", None)
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
# Suffix at the BOTTOM of the message
bottom_parts = []
# 1. Thinking blockquote at the bottom with model name
if settings.enable_thinking_display and thoughts:
thought_content = "\n".join(thoughts[-3:])
bottom_parts.append(format_thought(thought_content, model_name=model_display, lang=session.language).strip())
# 2. Active tools at the bottom
if settings.enable_tool_notifications and tools:
bottom_parts.append("\n".join(tools[-2:]).strip())
# 3. Status indicator at the bottom stating which model is being used and active topic
if not final:
status_lines = [f"🤔 <b>{escape_html(model_display)}</b> ▌"]
if conv_title:
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
bottom_parts.append("\n".join(status_lines))
if raw_text.strip():
formatted_body = markdown_to_telegram_html(raw_text)
if bottom_parts:
full_html = formatted_body + "\n\n" + "\n\n".join(bottom_parts)
else:
full_html = formatted_body + (" ▌" if not final else "")
else:
# Still waiting for response text - show project header and thinking at the bottom
proj_header = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
status_lines = [f"🤔 <b>{escape_html(model_display)}</b> ▌"]
if conv_title:
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
thinking_msg = "\n".join(status_lines)
if bottom_parts:
full_html = proj_header + "\n\n" + "\n\n".join(bottom_parts)
else:
full_html = proj_header + "\n\n" + thinking_msg
if len(full_html) > settings.max_message_length:
suffix_str = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
available_for_body = max(200, settings.max_message_length - len(suffix_str) - 60)
trimmed_raw = raw_text[:available_for_body]
formatted_body = markdown_to_telegram_html(trimmed_raw)
more_indicator = ("...\n<i>(ادامه در پیام بعدی...)</i>" if is_fa else "...\n<i>(continues...)</i>")
full_html = formatted_body + more_indicator + suffix_str
if len(full_html) > settings.max_message_length:
full_html = full_html[:settings.max_message_length - 10] + "..."
try:
await status_msg.edit_text(
full_html,
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
reply_markup=get_stop_button(session.language) if not final else None,
)
except BadRequest as e:
if "Message is not modified" not in str(e):
try:
fallback_body = escape_html(raw_text[:3500])
fallback_suffix = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
fallback_msg = (fallback_body + fallback_suffix)[:settings.max_message_length]
await status_msg.edit_text(
fallback_msg,
parse_mode=constants.ParseMode.HTML,
reply_markup=get_stop_button(session.language) if not final else None,
)
except Exception:
pass
except RetryAfter as e:
await asyncio.sleep(e.retry_after)
except Exception as e:
logger.debug(f"Streaming edit skipped: {e}")
def on_delta(token: str):
accumulated_tokens.append(token)
asyncio.create_task(update_telegram_display(final=False))
def on_thought(thought: str):
thoughts.append(thought)
asyncio.create_task(update_telegram_display(final=False))
def on_tool(name: str, args: str):
tools.append(format_tool_call(name, args, lang=session.language))
asyncio.create_task(update_telegram_display(final=False))
def on_model_change(new_model: str, new_effort: Optional[str] = None):
nonlocal model_display
model_display = get_model_display_name(new_model, new_effort)
asyncio.create_task(update_telegram_display(final=False))
def on_reset():
accumulated_tokens.clear()
async def send_typing_loop():
while True:
try:
await context.bot.send_chat_action(chat_id=chat_id, action=constants.ChatAction.TYPING)
await asyncio.sleep(4.5)
except asyncio.CancelledError:
break
except Exception:
await asyncio.sleep(4.5)
typing_task = asyncio.create_task(send_typing_loop())
async def run_agent_task():
try:
result = await AGYEngine.run_prompt(
session=session,
prompt=prompt,
on_delta=on_delta,
on_thought=on_thought,
on_tool=on_tool,
on_model_change=on_model_change,
on_reset=on_reset,
)
return result
finally:
typing_task.cancel()
task = asyncio.create_task(run_agent_task())
session_manager.active_tasks[chat_id] = task
try:
agent_result = await task
if isinstance(agent_result, AgentResult):
raw_text = agent_result.text
usage = agent_result.usage
duration = agent_result.duration
exec_model = agent_result.executed_model or curr_proj.model
exec_effort = agent_result.executed_effort or curr_proj.effort
else:
raw_text = agent_result if agent_result else "".join(accumulated_tokens)
usage = None
duration = 0.0
exec_model = curr_proj.model
exec_effort = curr_proj.effort
if not raw_text or not raw_text.strip() or raw_text.strip() == "<i>(Done with no text output)</i>":
if accumulated_tokens:
raw_text = "".join(accumulated_tokens)
else:
raw_text = "✅ **دستور و تغییرات درخواستی با موفقیت اعمال شدند.**" if is_fa else "✅ **Changes and requested operations were applied successfully.**"
# 1. Convert Markdown to Telegram HTML first
formatted_html = markdown_to_telegram_html(raw_text)
# 2. Intercept and execute all AI Bot Actions (Model switching, Effort, Languages, Projects, Tasks, Caddy, Buttons, Files, etc.)
formatted_html, created_tasks, executed_actions, inline_markup = await process_all_ai_actions(
raw_text=formatted_html,
chat_id=chat_id,
project_name=curr_proj.name,
is_fa=is_fa,
app=context.application,
)
chunks = split_message(formatted_html, max_length=settings.max_message_length)
if len(chunks) <= 1:
first_chunk = chunks[0] if chunks else "✅ <i>Done</i>"
try:
await status_msg.edit_text(first_chunk, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=inline_markup)
except Exception:
await status_msg.edit_text(escape_html(formatted_html[:settings.max_message_length]), parse_mode=constants.ParseMode.HTML, reply_markup=inline_markup)
else:
first_chunk = chunks[0] if chunks else "✅ <i>Done</i>"
try:
await status_msg.edit_text(first_chunk, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=None)
except Exception:
await status_msg.edit_text(escape_html(formatted_html[:settings.max_message_length]), parse_mode=constants.ParseMode.HTML, reply_markup=None)
for i, follow_up in enumerate(chunks[1:]):
is_last = (i == len(chunks[1:]) - 1)
chunk_markup = inline_markup if is_last else None
try:
await update.effective_message.reply_html(follow_up, disable_web_page_preview=True, reply_markup=chunk_markup)
except Exception:
await update.effective_message.reply_text(follow_up, reply_markup=chunk_markup)
# Mark successfully completed and delivered
session.turn_in_progress = False
session.last_delivered = True
session.last_response = raw_text
curr_proj.last_response = raw_text
session.last_status_msg_id = None
session.last_update_time = time.time()
session_manager.save()
# Send context length & usage stats in a separate message after task completes
if settings.enable_context_length_message:
reset_action_names = {"RESET_CONVERSATION", "NEW_CONV", "NEW_CHAT", "CLEAR_CONTEXT", "RESET_CHAT"}
is_reset = any(act in reset_action_names for act in executed_actions)
if is_reset or curr_proj.last_context_length is None:
stats_usage = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
else:
stats_usage = usage
if not stats_usage and curr_proj.last_context_length:
stats_usage = {
"input_tokens": curr_proj.last_context_length,
"total_tokens": curr_proj.last_total_tokens or curr_proj.last_context_length,
}
if stats_usage:
stats_html = format_context_stats(
usage=stats_usage,
duration=duration,
model=exec_model,
effort=exec_effort,
lang=session.language,
project_name=curr_proj.name,
conversation_id=curr_proj.conversation_id,
)
try:
await update.effective_message.reply_html(
stats_html,
reply_markup=get_usage_buttons(session.language),
disable_web_page_preview=True,
)
except Exception as stats_err:
logger.warning(f"Failed to send context stats message: {stats_err}")
except asyncio.CancelledError:
is_cancelled = True
session.turn_in_progress = False
session.last_delivered = True
session_manager.save()
try:
cancel_msg = "🛑 <b>عملیات و پردازش فعلی متوقف شد.</b>" if is_fa else "🛑 <b>Active task was stopped.</b>"
await status_msg.edit_text(cancel_msg, parse_mode=constants.ParseMode.HTML, reply_markup=None)
except Exception:
pass
except Exception as e:
session.turn_in_progress = False
session.last_delivered = True
session_manager.save()
logger.error(f"Error during agent turn: {e}", exc_info=True)
try:
await status_msg.edit_text(f"❌ <b>Error:</b> <code>{escape_html(str(e))}</code>", parse_mode=constants.ParseMode.HTML, reply_markup=None)
except Exception:
pass
finally:
session_manager.active_tasks.pop(chat_id, None)
typing_task.cancel()
if temp_file_to_cleanup:
try:
tf = Path(temp_file_to_cleanup)
if tf.exists():
tf.unlink()
except Exception as cleanup_err:
logger.warning(f"Failed to cleanup temp file {temp_file_to_cleanup}: {cleanup_err}")
async def on_startup(app: Application):
logger.info("Configuring Telegram bot command menu...")
try:
commands = [
BotCommand("start", "کنترل‌پنل اصلی / Main dashboard"),
BotCommand("memory", "حافظه هوش مصنوعی / AI Memory"),
BotCommand("projects", "مدیریت پروژه‌ها / Manage projects"),
BotCommand("newproject", "ساخت پروژه جدید / Create project"),
BotCommand("switch", "سوییچ پروژه / Switch project"),
BotCommand("tasks", "زمان‌بندی تسک‌ها / Scheduled tasks"),
BotCommand("schedule", "افزودن زمان‌بندی / Schedule task"),
BotCommand("conversations", "مدیریت گفتگوها / Conversations"),
BotCommand("switchconv", "سوییچ گفتگو / Switch chat"),
BotCommand("settopic", "تغییر موضوع گفتگو / Rename chat"),
BotCommand("delconv", "حذف گفتگو / Delete chat"),
BotCommand("clearconvs", "پاکسازی همه / Clear chats"),
BotCommand("lastconv", "آخرین گفتگو / Open last chat"),
BotCommand("share", "اشتراک‌گذاری پروژه / Share project"),
BotCommand("unshare", "لغو اشتراک پروژه / Unshare project"),
BotCommand("shared", "پروژه‌های اشتراکی / Shared projects"),
BotCommand("new", "گفتگوی جدید / New chat"),
BotCommand("compact", "فشرده‌سازی کانتکست / Compact context"),
BotCommand("usage", "سهمیه و مصرف / Quota & usage"),
BotCommand("account", "حساب کاربری AGY / AGY Account"),
BotCommand("login", "ورود به حساب گوگل / Google Login"),
BotCommand("logout", "خروج از حساب / Logout"),
BotCommand("model", "تغییر مدل / AI model"),
BotCommand("lang", "تنظیم زبان / Set language"),
BotCommand("effort", "سطح استدلال / Reasoning effort"),
BotCommand("workspace", "مسیر کاری / Workspace"),
BotCommand("status", "وضعیت ربات / System status"),
BotCommand("server", "سخت‌افزار سرور / RAM, CPU, Disk"),
BotCommand("context", "آمار کانتکست / Context & tokens"),
BotCommand("cancel", "توقف عملیات / Stop task"),
BotCommand("last", "آخرین خروجی / Last AI output"),
BotCommand("invite", "دعوت کاربر / Invite user"),
BotCommand("users", "لیست کاربران / Users list"),
BotCommand("git", "مخزن گیت / Git repository"),
BotCommand("sync", "همگام‌سازی گیت / Git sync"),
BotCommand("commit", "کامیت و پوش / Git commit"),
BotCommand("backup", "بکاپ پروژه / Project backup"),
BotCommand("upload", "آپلود وب فایل حجیم / Web upload"),
BotCommand("help", "راهنمای دستورات / Bot guide"),
]
await app.bot.set_my_commands(commands)
except Exception as cmd_err:
logger.warning(f"Failed to set bot commands: {cmd_err}")
# Set Telegram App in web uploader module & start Web Upload server
try:
set_telegram_app(app)
asyncio.create_task(start_web_uploader_server())
except Exception as ue:
logger.error(f"Failed to start web uploader server: {ue}", exc_info=True)
# Start the background task scheduler
task_scheduler.start(app)
# Automatically ensure all projects have Gitea repos and are synced
try:
asyncio.create_task(git_manager.sync_all_projects())
except Exception as ge:
logger.warning(f"Failed to trigger startup git sync: {ge}")
logger.info("Checking active sessions after startup...")
await asyncio.sleep(1.0)
for chat_id, session in list(session_manager.sessions.items()):
curr_proj = session_manager.get_current_project(chat_id)
proj_name = curr_proj.name if curr_proj else "default"
is_admin = chat_id in settings.admin_user_ids
try:
# Check if there was an interrupted or un-delivered turn
if session.turn_in_progress or not session.last_delivered or session.last_status_msg_id:
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
recovered_text = None
# Attempt to recover last output from AGY transcript
if curr_proj and curr_proj.conversation_id:
try:
from agy_engine import get_conversation_last_output
rec = get_conversation_last_output(curr_proj.conversation_id)
if rec.get("text"):
recovered_text = rec["text"]
elif rec.get("tools"):
tools_list = "\n".join(f"• <code>{escape_html(t)}</code>" for t in rec["tools"][-8:])
recovered_text = (
f"✅ <b>دستورات و ابزارهای زیر پیش از راه‌اندازی مجدد با موفقیت اعمال شدند:</b>\n\n{tools_list}"
if is_fa else
f"✅ <b>The following operations were executed before restart:</b>\n\n{tools_list}"
)
except Exception as rec_err:
logger.debug(f"Startup recovery failed for {chat_id}: {rec_err}")
if recovered_text:
formatted_html = markdown_to_telegram_html(recovered_text)
rec_header = (
f"🟢 <b>[پاسخ بازیابی‌شده پس از ری‌استارت ربات - پروژه: <code>{escape_html(proj_name)}</code>]</b>\n\n"
if is_fa else
f"🟢 <b>[Response Recovered After Restart - Project: <code>{escape_html(proj_name)}</code>]</b>\n\n"
)
full_delivered = rec_header + formatted_html
chunks = split_message(full_delivered, max_length=settings.max_message_length)
if session.last_status_msg_id:
try:
await app.bot.edit_message_text(
chat_id=chat_id,
message_id=session.last_status_msg_id,
text=chunks[0],
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
)
for extra in chunks[1:]:
await app.bot.send_message(
chat_id=chat_id,
text=extra,
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
)
except Exception:
for c in chunks:
try:
await app.bot.send_message(
chat_id=chat_id,
text=c,
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
)
except Exception:
pass
else:
for c in chunks:
try:
await app.bot.send_message(
chat_id=chat_id,
text=c,
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
)
except Exception:
pass
session.last_response = recovered_text
if curr_proj:
curr_proj.last_response = recovered_text
elif is_admin and session.last_status_msg_id:
ready_msg = (
f"🟢 <b>ربات راه‌اندازی مجدد شد و آنلاین است (پروژه فعال: <code>{escape_html(proj_name)}</code>).</b>\n\nآماده دریافت دستورات جدید شما هستم!"
if is_fa else
f"🟢 <b>AGY Bot restarted successfully and is online (Active Project: <code>{escape_html(proj_name)}</code>)!</b>\n\nReady for your commands!"
)
try:
await app.bot.edit_message_text(
chat_id=chat_id,
message_id=session.last_status_msg_id,
text=ready_msg,
parse_mode=constants.ParseMode.HTML,
disable_web_page_preview=True,
)
except Exception as edit_err:
logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}")
except Exception as e:
logger.error(f"Failed to deliver recovery message to chat {chat_id}: {e}", exc_info=True)
finally:
# Reset turn flags cleanly
session.turn_in_progress = False
session.restart_pending = False
session.last_delivered = True
session.last_status_msg_id = None
session_manager.save()
def main():
if not settings.telegram_bot_token:
logger.error("TELEGRAM_BOT_TOKEN is not set!")
sys.exit(1)
logger.info("Initializing Telegram Bot Application with concurrent_updates enabled...")
application = (
Application.builder()
.token(settings.telegram_bot_token)
.concurrent_updates(True)
.post_init(on_startup)
.build()
)
# Project Management Handlers
application.add_handler(CommandHandler(["projects", "project", "p"], projects_command))
application.add_handler(CommandHandler(["newproject", "createproject"], new_project_command))
application.add_handler(CommandHandler(["switch", "use", "switchproject"], switch_project_command))
application.add_handler(CommandHandler(["delproject", "deleteproject"], delete_project_command))
application.add_handler(CommandHandler("renameproject", rename_project_command))
application.add_handler(CommandHandler(["share", "shareproject"], share_command))
application.add_handler(CommandHandler(["unshare", "unshareproject"], unshare_command))
application.add_handler(CommandHandler(["shared", "sharedprojects"], shared_command))
application.add_handler(CommandHandler(["backup", "zip", "export"], backup_command))
application.add_handler(CommandHandler(["upload", "drop", "webupload"], upload_command))
# Git & Gitea Version Control Handlers
application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_command))
application.add_handler(CommandHandler(["publish", "gitpublish", "release"], publish_command))
application.add_handler(CommandHandler(["ftp", "ftpdeploy", "deploy"], ftp_command))
application.add_handler(CommandHandler(["ssh", "remotedev", "sshdev", "remote"], ssh_command))
application.add_handler(CommandHandler(["sync", "gitsync", "pull"], sync_command))
application.add_handler(CommandHandler(["commit", "gitcommit"], commit_command))
application.add_handler(CommandHandler(["undo", "revert", "gitundo", "gitrevert", "laghv"], undo_command))
# Scheduled Tasks Handlers
application.add_handler(CommandHandler(["tasks", "task", "schedule", "schedules", "cron", "timer"], tasks_command))
# Admin User & Invite Handlers
application.add_handler(CommandHandler(["invite", "add_user", "adduser"], invite_command))
application.add_handler(CommandHandler(["uninvite", "revoke", "ban", "remove_user", "deluser"], uninvite_command))
application.add_handler(CommandHandler(["users", "whitelist", "members"], users_command))
application.add_handler(CommandHandler(["invitelink", "invite_link"], invitelink_command))
# General & Control Panel Handlers
application.add_handler(CommandHandler(["start", "menu", "dashboard", "panel"], start_command))
application.add_handler(CommandHandler(["memory", "memories", "hafeze"], memory_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler(["usage", "quota", "credits"], usage_command))
application.add_handler(CommandHandler(["account", "profile_auth", "acc"], account_command))
application.add_handler(CommandHandler(["login", "google_login", "signin"], login_command))
application.add_handler(CommandHandler(["logout", "signout"], logout_command))
application.add_handler(CommandHandler(["new", "reset"], new_command))
application.add_handler(CommandHandler(["compact", "compress", "fashorde"], compact_command))
application.add_handler(CommandHandler(["lastconv", "lastconversation", "openlast", "resume", "open_last"], last_conversation_command))
application.add_handler(CommandHandler(["conversations", "convs", "history_conv", "dialogs", "chats"], conversations_command))
application.add_handler(CommandHandler(["switchconv", "switch_conv", "openconv", "useconv", "selectconv"], switch_conv_command))
application.add_handler(CommandHandler(["settopic", "set_topic", "settitle", "set_title", "convtitle", "title"], set_conv_title_command))
application.add_handler(CommandHandler(["delconv", "deleteconv", "del_conv", "delete_conv", "removeconv"], delete_conv_command))
application.add_handler(CommandHandler(["clearconvs", "clear_convs", "deleteallconvs"], clear_convs_command))
application.add_handler(CommandHandler("model", model_command))
application.add_handler(CommandHandler(["lang", "language"], lang_command))
application.add_handler(CommandHandler("effort", effort_command))
application.add_handler(CommandHandler("workspace", workspace_command))
application.add_handler(CommandHandler("status", status_command))
application.add_handler(CommandHandler(["server", "hardware", "ram", "cpu", "sysinfo", "disk", "stats"], server_hardware_command))
application.add_handler(CommandHandler(["last", "output"], last_command))
application.add_handler(CommandHandler(["context", "tokens"], context_command))
application.add_handler(CommandHandler("cancel", cancel_command))
application.add_handler(CommandHandler(["restart", "reload"], restart_command))
application.add_handler(CommandHandler("auth", auth_command))
application.add_handler(CommandHandler("exec", exec_command))
# Callback Query Handler
application.add_handler(CallbackQueryHandler(callback_handler))
# Message Handlers
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler))
application.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO | filters.VOICE | filters.AUDIO, file_handler))
# Global Error Handler
async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
logger.error("Unhandled exception in telegram update handler:", exc_info=context.error)
application.add_error_handler(global_error_handler)
logger.info("Starting Telegram Bot Polling...")
application.run_polling(drop_pending_updates=False)
if __name__ == "__main__":
main()