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

5984 lines
308 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,
format_git_info,
format_git_commits_page,
format_commit_detail_view,
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 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 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)
# 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 ""
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>مدل هوش مصنوعی:</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("📈 سهمیه مصرف (AGY)", callback_data="btn_usage_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>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("📈 Quota & Usage", callback_data="btn_usage_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("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"),
],
[
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("📜 Recent Commits (History)", callback_data="git_hist:1"),
],
[
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_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}"),
])
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("✅ این کامیت هم‌اکنون فعال است" 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="proj_ws_info"),
InlineKeyboardButton("🗑️ حذف پروژه", callback_data="proj_del_menu"),
])
keyboard.append([
InlineKeyboardButton("🔄 ری‌استارت گفتگو", callback_data="btn_restart"),
InlineKeyboardButton("🧠 انتخاب مدل", callback_data="proj_model_menu"),
])
keyboard.append([
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
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("📂 Workspace Info", callback_data="proj_ws_info"),
InlineKeyboardButton("🗑️ Delete Project", callback_data="proj_del_menu"),
])
keyboard.append([
InlineKeyboardButton("🔄 Reset Chat", callback_data="btn_restart"),
InlineKeyboardButton("🧠 Switch Model", callback_data="proj_model_menu"),
])
keyboard.append([
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
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_lang_menu"),
InlineKeyboardButton("📊 وضعیت و کانتکست", callback_data="btn_status_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("🌐 Response Language", callback_data="btn_lang_menu"),
InlineKeyboardButton("📊 Status & Tokens", callback_data="btn_status_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)
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)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🔄 بروزرسانی مصرف", callback_data="btn_usage_refresh"),
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("🖥 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 a conversation to switch, or tap 🗑️ to 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 = "🟢 <b>[گفتگوی فعال]</b> " if is_curr else f"#{idx} "
prompt_snippet = c.get("first_prompt") or "(بدون متن)"
prompt_clean = prompt_snippet.replace("\n", " ").strip()
if len(prompt_clean) > 40:
prompt_clean = prompt_clean[:37] + "..."
turns = c.get("turns_count", 0)
if is_fa:
body_lines.append(f"{badge}<code>{cid[:8]}...</code> ({turns} نوبت)\n └ <i>«{escape_html(prompt_clean)}»</i>\n")
else:
body_lines.append(f"{badge}<code>{cid[:8]}...</code> ({turns} turns)\n └ <i>\"{escape_html(prompt_clean)}\"</i>\n")
if is_curr:
btn_label = f"🟢 #{idx}: {prompt_clean} (فعال)" if is_fa else f"🟢 #{idx}: {prompt_clean} (Active)"
btn_action = f"conv_view_{cid}"
else:
btn_label = f"💬 #{idx}: {prompt_clean}"
btn_action = f"conv_switch_{cid}"
keyboard.append([
InlineKeyboardButton(btn_label[:38], callback_data=btn_action),
InlineKeyboardButton("🗑️", callback_data=f"conv_del_ask_{cid}"),
])
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>/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"• 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 "(شروع مکالمه)"
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:
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"📝 <b>موضوع / اولین پیام:</b>\n<i>«{escape_html(first_prompt[:250])}»</i>\n"
)
if last_resp:
text += f"\n📋 <b>آخرین پاسخ AI:</b>\n<i>«{escape_html(last_resp)}»</i>\n"
else:
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"📝 <b>First Prompt / Topic:</b>\n<i>\"{escape_html(first_prompt[:250])}\"</i>\n"
)
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_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>⏰ زمان‌بندی تسک‌ها (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"• <code>/workspace [مسیر]</code> - تغییر دایرکتوری کاری پروژه فعال\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)
# 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):
"""Triggers an agent turn from external event (like Web Uploader)."""
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
# Create dummy update/context wrapper or send initial status message
initial_status = (
f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n\n"
f"💭 <i>در حال پردازش فایل‌های آپلود شده روی پروژه <b>{escape_html(curr_proj.name)}</b>...</i>"
if is_fa else
f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n\n"
f"💭 <i>Processing uploaded files for <b>{escape_html(curr_proj.name)}</b>...</i>"
)
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
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"
keyboard = [
[
InlineKeyboardButton(compact_label, callback_data="btn_compact"),
InlineKeyboardButton(restart_label, callback_data="btn_restart"),
]
]
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
target_cid = curr_proj.conversation_id 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)}</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: /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: /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: /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: /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 (فارسی)")
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_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 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_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
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>{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_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 (فارسی)")
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
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 (فارسی)")
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
session_manager.cancel_active_task(chat_id)
# Initial thinking status message clearly stating which project is active
if status_msg_to_reuse:
status_msg = status_msg_to_reuse
else:
if is_fa:
initial_status = (
f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n\n"
f"💭 <i>در حال تفکر روی پروژه <b>{escape_html(curr_proj.name)}</b>...</i>"
)
else:
initial_status = (
f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n\n"
f"💭 <i>Thinking on project <b>{escape_html(curr_proj.name)}</b>...</i>"
)
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
# Suffix at the BOTTOM of the message
bottom_parts = []
# 1. Thinking blockquote at the bottom with project name
if settings.enable_thinking_display and thoughts:
thought_content = "\n".join(thoughts[-3:])
bottom_parts.append(format_thought(thought_content, project_name=curr_proj.name, 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 project is being thought on
if not final:
if is_fa:
bottom_parts.append(f"💭 <i>در حال تفکر روی پروژه <b>{escape_html(curr_proj.name)}</b>...</i> ▌")
else:
bottom_parts.append(f"💭 <i>Thinking on project <b>{escape_html(curr_proj.name)}</b>...</i> ▌")
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>"
if bottom_parts:
full_html = proj_header + "\n\n" + "\n\n".join(bottom_parts)
else:
thinking_msg = f"💭 <i>در حال تفکر روی پروژه <b>{escape_html(curr_proj.name)}</b>...</i> ▌" if is_fa else f"💭 <i>Thinking on project <b>{escape_html(curr_proj.name)}</b>...</i> ▌"
full_html = proj_header + "\n\n" + thinking_msg
if len(full_html) <= settings.max_message_length:
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:
await status_msg.edit_text(
escape_html(raw_text[:3800]),
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))
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,
)
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, Files, etc.)
formatted_html, created_tasks, executed_actions = 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)
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 follow_up in chunks[1:]:
try:
await update.effective_message.reply_html(follow_up, disable_web_page_preview=True)
except Exception:
await update.effective_message.reply_text(follow_up)
# 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("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("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("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:
# Clean up pending status message if interrupted
if is_admin and session.last_status_msg_id:
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
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:
# Delete/Clear last message and turn flags from memory
session.last_response = None
session.last_prompt = None
if curr_proj:
curr_proj.last_response = None
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...")
application = (
Application.builder()
.token(settings.telegram_bot_token)
.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(["sync", "gitsync", "pull"], sync_command))
application.add_handler(CommandHandler(["commit", "gitcommit"], commit_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("help", help_command))
application.add_handler(CommandHandler(["usage", "quota", "credits"], usage_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(["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))
logger.info("Starting Telegram Bot Polling...")
application.run_polling(drop_pending_updates=True)
if __name__ == "__main__":
main()