7095 lines
370 KiB
Python
7095 lines
370 KiB
Python
import os
|
||
import sys
|
||
import re
|
||
import json
|
||
import time
|
||
import shutil
|
||
import asyncio
|
||
import logging
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
|
||
from telegram import (
|
||
Update,
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
constants,
|
||
BotCommand,
|
||
)
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
MessageHandler,
|
||
CallbackQueryHandler,
|
||
ContextTypes,
|
||
filters,
|
||
)
|
||
from telegram.error import TelegramError, BadRequest, RetryAfter
|
||
|
||
from config import settings, ENV_FILE
|
||
from agy_engine import (
|
||
AGYEngine,
|
||
AgentResult,
|
||
session_manager,
|
||
AVAILABLE_MODELS,
|
||
AVAILABLE_EFFORTS,
|
||
AVAILABLE_LANGUAGES,
|
||
strip_ansi,
|
||
get_conversation_last_output,
|
||
get_conversation_metadata,
|
||
clean_user_prompt,
|
||
)
|
||
from formatters import (
|
||
markdown_to_telegram_html,
|
||
format_thought,
|
||
format_tool_call,
|
||
format_context_stats,
|
||
get_model_display_name,
|
||
format_git_info,
|
||
format_git_commits_page,
|
||
format_commit_detail_view,
|
||
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 <نام_پروژه></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 <project_name></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("🧠 حافظه هوش مصنوعی", callback_data="btn_memory_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("🧠 AI Memory", callback_data="btn_memory_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("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🔄 همگامسازی گیت (Sync)", callback_data="btn_git_sync"),
|
||
InlineKeyboardButton("💾 ثبت کامیت و پوش (Commit)", callback_data="btn_git_commit"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("⬇️ دریافت تغییرات (Pull)", callback_data="btn_git_pull"),
|
||
InlineKeyboardButton("📦 دانلود سورس (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📁 مدیریت پروژهها", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🌐 Open in Browser (Gitea)", url=urls["web_url"]),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"),
|
||
InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🔄 Git Sync", callback_data="btn_git_sync"),
|
||
InlineKeyboardButton("💾 Commit & Push", callback_data="btn_git_commit"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("⬇️ Git Pull", callback_data="btn_git_pull"),
|
||
InlineKeyboardButton("📦 Download Zip", callback_data=f"proj_backup:{curr_proj.name}"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📁 Project Manager", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Generates paginated commit history view."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
|
||
|
||
status_info = await git_manager.git_status(curr_proj.workspace)
|
||
branch = status_info.get("branch") or "main"
|
||
all_commits = await git_manager.get_commit_history(curr_proj.workspace, limit=20)
|
||
|
||
per_page = 5
|
||
total_commits = len(all_commits)
|
||
total_pages = max(1, (total_commits + per_page - 1) // per_page)
|
||
current_page = max(1, min(page, total_pages))
|
||
|
||
start_idx = (current_page - 1) * per_page
|
||
page_commits = all_commits[start_idx:start_idx + per_page]
|
||
|
||
text = format_git_commits_page(
|
||
project_name=curr_proj.name,
|
||
commits=page_commits,
|
||
current_page=current_page,
|
||
total_pages=total_pages,
|
||
branch=branch,
|
||
lang=session.language,
|
||
)
|
||
|
||
keyboard = []
|
||
for c in page_commits:
|
||
short_h = c.get("short_hash", "")
|
||
subj = c.get("subject", "")
|
||
if len(subj) > 28:
|
||
subj = subj[:25] + "..."
|
||
is_curr = c.get("is_current", False)
|
||
prefix = "📍" if is_curr else "🔖"
|
||
btn_text = f"{prefix} {short_h}: {subj}"
|
||
keyboard.append([InlineKeyboardButton(btn_text, callback_data=f"git_view:{short_h}")])
|
||
|
||
nav_row = []
|
||
if current_page > 1:
|
||
nav_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"git_hist:{current_page - 1}"))
|
||
nav_row.append(InlineKeyboardButton(f"📄 {current_page}/{total_pages}", callback_data=f"git_hist:{current_page}"))
|
||
if current_page < total_pages:
|
||
nav_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"git_hist:{current_page + 1}"))
|
||
if nav_row:
|
||
keyboard.append(nav_row)
|
||
|
||
action_row = []
|
||
if branch != "main":
|
||
action_row.append(InlineKeyboardButton("🌿 برگشت به main" if is_fa else "🌿 Return to main", callback_data="git_co_main"))
|
||
action_row.append(InlineKeyboardButton("🔄 بروزرسانی" if is_fa else "🔄 Refresh", callback_data=f"git_hist:{current_page}"))
|
||
keyboard.append(action_row)
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔙 بازگشت به منوی گیت" if is_fa else "🔙 Back to Git Menu", callback_data="btn_git_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
async def build_git_commit_detail_menu(chat_id: int, commit_hash: str) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Generates view for a single commit details with action buttons."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ پروژهای یافت نشد." if is_fa else "⚠️ No project found."
|
||
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")]])
|
||
|
||
detail = await git_manager.get_commit_detail(curr_proj.workspace, commit_hash)
|
||
if "error" in detail:
|
||
err_msg = f"⚠️ خطا در بارگذاری کامیت: {detail['error']}" if is_fa else f"⚠️ Error loading commit: {detail['error']}"
|
||
return err_msg, InlineKeyboardMarkup([[InlineKeyboardButton("🔙 بازگشت به تاریخچه", callback_data="git_hist:1")]])
|
||
|
||
urls = git_manager.get_repo_urls(curr_proj.name)
|
||
text = format_commit_detail_view(
|
||
project_name=curr_proj.name,
|
||
commit_detail=detail,
|
||
web_url=urls["web_url"],
|
||
lang=session.language,
|
||
)
|
||
|
||
short_h = detail.get("short_hash", commit_hash)
|
||
is_curr = detail.get("is_current", False)
|
||
|
||
keyboard = []
|
||
if not is_curr:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔀 سوییچ به این کامیت (Checkout)" if is_fa else "🔀 Switch to this commit (Checkout)", callback_data=f"git_co:{short_h}"),
|
||
InlineKeyboardButton("↩️ لغو این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("⏪ بازگردانی پروژه به این کامیت (Reset)" if is_fa else "⏪ Restore Project to this Commit (Reset)", callback_data=f"git_ask_reset:{short_h}"),
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("↩️ لغو تغییرات این کامیت (Revert)" if is_fa else "↩️ Revert This Commit", callback_data=f"git_ask_revert:{short_h}"),
|
||
InlineKeyboardButton("✅ کامیت فعال فعلی" if is_fa else "✅ Currently Active Commit", callback_data=f"git_view:{short_h}"),
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌿 سوییچ به شاخه اصلی (main)" if is_fa else "🌿 Switch to main", callback_data="git_co_main"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📜 لیست کامیتها" if is_fa else "📜 Commit List", callback_data="git_hist:1"),
|
||
InlineKeyboardButton("🔙 منوی گیت" if is_fa else "🔙 Git Menu", callback_data="btn_git_menu"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
def build_projects_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Generates the interactive Project Manager text and keyboard markup."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_admin_user = settings.is_admin(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
own_projs = session_manager.get_user_projects(chat_id)
|
||
shared_projs = session_manager.get_shared_projects(chat_id)
|
||
accessible = session_manager.get_all_accessible_projects(chat_id)
|
||
|
||
if not accessible or not curr_proj:
|
||
if is_fa:
|
||
text = (
|
||
"📁 <b>مدیریت پروژهها (Project Manager)</b>\n\n"
|
||
"⚠️ <b>شما هنوز هیچ پروژهای ایجاد نکردهاید!</b>\n"
|
||
"برای شروع گفتگو و کدنویسی با هوش مصنوعی، لطفاً ابتدا یک پروژه اختصاصی بسازید.\n\n"
|
||
"💡 <i>دستور ایجاد سریع پروژه:</i>\n"
|
||
"<code>/newproject <نام_پروژه></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 <project_name></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 <نام></code>\n"
|
||
f"<i>اشتراکگذاری:</i> <code>/share <user_id></code>\n"
|
||
f"<i>سوییچ سریع:</i> <code>/switch <نام></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 <name></code>\n"
|
||
f"<i>Share:</i> <code>/share <user_id></code>\n"
|
||
f"<i>Quick switch:</i> <code>/switch <name></code>"
|
||
)
|
||
|
||
# Build keyboard
|
||
keyboard = []
|
||
proj_row = []
|
||
for key, p in accessible.items():
|
||
is_active = (key == session.active_project or p.name == session.active_project)
|
||
is_shared = (p.owner_id != chat_id and p.name != "default")
|
||
icon = "✅" if is_active else ("👥" if is_shared else "📁")
|
||
btn_label = f"{icon} {p.name}"
|
||
proj_row.append(InlineKeyboardButton(btn_label, callback_data=f"proj_switch:{key}"))
|
||
if len(proj_row) == 2:
|
||
keyboard.append(proj_row)
|
||
proj_row = []
|
||
if proj_row:
|
||
keyboard.append(proj_row)
|
||
|
||
if is_fa:
|
||
keyboard.append([
|
||
InlineKeyboardButton("➕ پروژه جدید", callback_data="proj_new"),
|
||
InlineKeyboardButton("📤 آپلود فایل (وب)", callback_data=f"proj_upload_link:{curr_proj.name}"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📦 دریافت فایل بکاپ (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
|
||
InlineKeyboardButton("🤝 اشتراکگذاری", callback_data="proj_share_menu"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🧠 حافظه پروژه", callback_data="mem_tab:project:0"),
|
||
InlineKeyboardButton("📂 مسیر کاری", callback_data="proj_ws_info"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🗑️ حذف پروژه", callback_data="proj_del_menu"),
|
||
InlineKeyboardButton("🧠 انتخاب مدل", callback_data="proj_model_menu"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔄 ریاستارت گفتگو", callback_data="btn_restart"),
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔙 بستن منو", callback_data="proj_close"),
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("➕ New Project", callback_data="proj_new"),
|
||
InlineKeyboardButton("📤 Web Upload", callback_data=f"proj_upload_link:{curr_proj.name}"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📦 Backup Project (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
|
||
InlineKeyboardButton("🤝 Share Project", callback_data="proj_share_menu"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🧠 Project Memory", callback_data="mem_tab:project:0"),
|
||
InlineKeyboardButton("📂 Workspace Info", callback_data="proj_ws_info"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🗑️ Delete Project", callback_data="proj_del_menu"),
|
||
InlineKeyboardButton("🧠 Switch Model", callback_data="proj_model_menu"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔄 Reset Chat", callback_data="btn_restart"),
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
def build_sharing_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Generates the interactive Project Sharing text and keyboard markup."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_admin_user = settings.is_admin(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
return build_projects_menu(chat_id)
|
||
|
||
is_owner = (curr_proj.owner_id == chat_id or (curr_proj.name == "default" and is_admin_user))
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"🤝 <b>مدیریت اشتراکگذاری پروژه (Project Sharing)</b>\n\n"
|
||
f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"👑 <b>مالک:</b> <code>{curr_proj.owner_id or 'سیستم'}</code> {'(شما)' if is_owner else ''}\n"
|
||
f"📂 <b>مسیر:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
|
||
)
|
||
if not is_owner and not is_admin_user:
|
||
text += (
|
||
"⚠️ <i>شما مالک این پروژه نیستید و امکان مدیریت دسترسیهای آن را ندارید.</i>\n\n"
|
||
"برای اشتراکگذاری، ابتدا به یکی از پروژههای شخصی خود سوییچ نمایید."
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("📁 بازگشت به پروژهها", callback_data="proj_menu")],
|
||
[InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard")],
|
||
]
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
if curr_proj.shared_with:
|
||
text += f"👥 <b>کاربران دارای دسترسی ({len(curr_proj.shared_with)}):</b>\n"
|
||
for uid in curr_proj.shared_with:
|
||
text += f"• <code>{uid}</code>\n"
|
||
else:
|
||
text += "👥 <b>کاربران دارای دسترسی:</b> <i>هیچ کاربری اضافه نشده است (فقط شما).</i>\n"
|
||
|
||
text += (
|
||
"\n──────────────\n"
|
||
"💡 <b>نحوه اشتراکگذاری با کاربر دیگر:</b>\n"
|
||
"دستور زیر را در چت ارسال کنید:\n"
|
||
f"<code>/share <شناسه_کاربری></code>\n\n"
|
||
"<b>مثال:</b> <code>/share 123456789</code>\n\n"
|
||
"💡 <b>لغو اشتراک با یک کاربر:</b>\n"
|
||
"<code>/unshare <شناسه_کاربری></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 <user_id></code>\n\n"
|
||
"<b>Example:</b> <code>/share 123456789</code>\n\n"
|
||
"💡 <b>To revoke access:</b>\n"
|
||
"<code>/unshare <user_id></code>"
|
||
)
|
||
|
||
keyboard = []
|
||
if is_owner or is_admin_user:
|
||
for uid in curr_proj.shared_with:
|
||
lbl = f"❌ لغو اشتراک {uid}" if is_fa else f"❌ Revoke {uid}"
|
||
keyboard.append([InlineKeyboardButton(lbl, callback_data=f"proj_unshare_confirm:{uid}")])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("📁 بازگشت به پروژهها" if is_fa else "📁 Back to Projects", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
def build_settings_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Generates the interactive Settings & AI Configuration submenu."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
text = "⚠️ هیچ پروژهای فعال نیست." if is_fa else "⚠️ No active project."
|
||
keyboard = [[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]]
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"⚙️ <b>تنظیمات پروژه و هوش مصنوعی</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>مدل هوش مصنوعی:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
|
||
f"• 🌐 <b>زبان پاسخدهی:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
|
||
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
|
||
f"💡 <i>از گزینههای زیر برای پیکربندی استفاده کنید:</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🧠 انتخاب مدل هوش مصنوعی", callback_data="proj_model_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("⚡ سطح تفکر / استدلال (Effort)", callback_data="btn_effort_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🧠 مدیریت حافظه هوش مصنوعی", callback_data="btn_memory_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🌐 زبان پاسخدهی", callback_data="btn_lang_menu"),
|
||
InlineKeyboardButton("📊 وضعیت و کانتکست", callback_data="btn_status_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📦 دریافت فایل بکاپ (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
|
||
InlineKeyboardButton("🤝 اشتراکگذاری پروژه", callback_data="proj_share_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🏠 بازگشت به منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
text = (
|
||
f"⚙️ <b>Project & AI Settings</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>AI Model:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• ⚡ <b>Reasoning Effort:</b> <code>{curr_proj.effort}</code>\n"
|
||
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
|
||
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
|
||
f"💡 <i>Select an option below to configure:</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🧠 Select AI Model", callback_data="proj_model_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("⚡ Reasoning Effort", callback_data="btn_effort_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🧠 AI Memory Management", callback_data="btn_memory_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🌐 Response Language", callback_data="btn_lang_menu"),
|
||
InlineKeyboardButton("📊 Status & Tokens", callback_data="btn_status_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📦 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 any conversation button to switch, view details, or delete:</i>\n\n"
|
||
)
|
||
|
||
if not convs:
|
||
empty_str = "<i>هنوز گفتگویی در این پروژه ثبت نشده است. با ارسال اولین پیام، گفتگو ساخته میشود.</i>" if is_fa else "<i>No conversations recorded yet. Send a message to start one.</i>"
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🔄 گفتگوی جدید" if is_fa else "🔄 New Chat", callback_data="btn_restart"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
]
|
||
]
|
||
return header + empty_str, InlineKeyboardMarkup(keyboard)
|
||
|
||
body_lines = []
|
||
keyboard = []
|
||
|
||
for idx, c in enumerate(convs[:8], start=1):
|
||
cid = c["id"]
|
||
is_curr = c.get("is_current", False)
|
||
badge_text = "🟢 [فعال]" if is_curr else f"#{idx}"
|
||
custom_t = c.get("title")
|
||
prompt_snippet = custom_t if custom_t else (c.get("first_prompt") or "(بدون متن)")
|
||
prompt_clean = prompt_snippet.replace("\n", " ").strip()
|
||
|
||
# Format text preview for message body (up to 80 chars)
|
||
prompt_body_preview = prompt_clean if len(prompt_clean) <= 80 else prompt_clean[:77] + "..."
|
||
|
||
# Format label for full-width button (up to 45 chars)
|
||
prompt_btn_preview = prompt_clean if len(prompt_clean) <= 45 else prompt_clean[:42] + "..."
|
||
|
||
turns = c.get("turns_count", 0)
|
||
title_prefix = "🏷️ " if custom_t else ""
|
||
if is_fa:
|
||
status_tag = " <b>(در حال استفاده)</b>" if is_curr else ""
|
||
body_lines.append(f"<b>{idx}.</b> {badge_text}{status_tag} <code>{cid[:8]}</code> ({turns} نوبت)\n └ {title_prefix}<i>«{escape_html(prompt_body_preview)}»</i>\n")
|
||
else:
|
||
status_tag = " <b>(Active)</b>" if is_curr else ""
|
||
body_lines.append(f"<b>{idx}.</b> {badge_text}{status_tag} <code>{cid[:8]}</code> ({turns} turns)\n └ {title_prefix}<i>\"{escape_html(prompt_body_preview)}\"</i>\n")
|
||
|
||
if is_curr:
|
||
btn_label = f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (فعال)" if is_fa else f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (Active)"
|
||
btn_action = f"conv_view_{cid}"
|
||
else:
|
||
btn_label = f"💬 #{idx}: {title_prefix}{prompt_btn_preview}"
|
||
btn_action = f"conv_switch_{cid}"
|
||
|
||
# Full width button for maximum readability
|
||
keyboard.append([
|
||
InlineKeyboardButton(btn_label[:60], callback_data=btn_action),
|
||
])
|
||
|
||
action_row = [
|
||
InlineKeyboardButton("⏮️ آخرین گفتگو" if is_fa else "⏮️ Last Chat", callback_data="btn_conv_last"),
|
||
InlineKeyboardButton("🔄 گفتگوی جدید" if is_fa else "🔄 New Chat", callback_data="btn_restart"),
|
||
]
|
||
keyboard.append(action_row)
|
||
|
||
if convs:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🗑️ پاکسازی تمام گفتگوها" if is_fa else "🗑️ Clear All Conversations", callback_data="conv_clear_all_ask"),
|
||
])
|
||
|
||
nav_row = [
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
|
||
]
|
||
keyboard.append(nav_row)
|
||
|
||
if is_fa:
|
||
footer = (
|
||
f"\n💡 <i>دستورات متنی:</i>\n"
|
||
f"• سوییچ: <code>/switchconv <شماره یا شناسه></code>\n"
|
||
f"• تغییر موضوع: <code>/settopic <عنوان جدید></code>\n"
|
||
f"• حذف: <code>/delconv <شماره یا شناسه></code>\n"
|
||
f"• پاکسازی همه: <code>/clearconvs</code>"
|
||
)
|
||
else:
|
||
footer = (
|
||
f"\n💡 <i>Text commands:</i>\n"
|
||
f"• Switch: <code>/switchconv <number or ID></code>\n"
|
||
f"• Rename: <code>/settopic <new title></code>\n"
|
||
f"• Delete: <code>/delconv <number or ID></code>\n"
|
||
f"• Clear all: <code>/clearconvs</code>"
|
||
)
|
||
|
||
return header + "".join(body_lines) + footer, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
def build_conversation_detail_menu(chat_id: int, conv_id: str) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Builds a detailed view of a single conversation with switch and delete options."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
text = "⚠️ هیچ پروژهای یافت نشد." if is_fa else "⚠️ No active project found."
|
||
return text, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
|
||
|
||
meta = get_conversation_metadata(conv_id)
|
||
out = get_conversation_last_output(conv_id)
|
||
is_active = (curr_proj.conversation_id == conv_id)
|
||
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
|
||
custom_title = getattr(curr_proj, "conversation_titles", {}).get(conv_id) if hasattr(curr_proj, "conversation_titles") else None
|
||
display_title = custom_title if custom_title else first_prompt
|
||
last_resp = out.get("text") or meta.get("last_response") or ""
|
||
if len(last_resp) > 200:
|
||
last_resp = last_resp[:197] + "..."
|
||
|
||
status_str = "🟢 فعال (در حال استفاده)" if is_active else "⚪ غیرفعال (بایگانی شده)"
|
||
status_str_en = "🟢 Active (In use)" if is_active else "⚪ Inactive (Archived)"
|
||
|
||
if is_fa:
|
||
title_section = f"• 🏷️ <b>عنوان / موضوع گفتگو:</b> <b>{escape_html(custom_title)}</b>\n• 📝 <b>اولین پیام:</b> <i>«{escape_html(first_prompt[:200])}»</i>\n" if custom_title else f"📝 <b>موضوع / اولین پیام:</b>\n<i>«{escape_html(first_prompt[:250])}»</i>\n"
|
||
text = (
|
||
f"🔍 <b>جزئیات گفتگو (Conversation Details)</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>شناسه گفتگو:</b> <code>{conv_id}</code>\n"
|
||
f"• 🏷 <b>وضعیت:</b> {status_str}\n"
|
||
f"• 🔢 <b>تعداد نوبتها:</b> <code>{meta.get('turns_count', 0)} نوبت</code>\n\n"
|
||
f"{title_section}"
|
||
)
|
||
if last_resp:
|
||
text += f"\n📋 <b>آخرین پاسخ AI:</b>\n<i>«{escape_html(last_resp)}»</i>\n"
|
||
else:
|
||
title_section = f"• 🏷️ <b>Topic / Title:</b> <b>{escape_html(custom_title)}</b>\n• 📝 <b>First Prompt:</b> <i>\"{escape_html(first_prompt[:200])}\"</i>\n" if custom_title else f"📝 <b>First Prompt / Topic:</b>\n<i>\"{escape_html(first_prompt[:250])}\"</i>\n"
|
||
text = (
|
||
f"🔍 <b>Conversation Details</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Conversation ID:</b> <code>{conv_id}</code>\n"
|
||
f"• 🏷 <b>Status:</b> {status_str_en}\n"
|
||
f"• 🔢 <b>Turns:</b> <code>{meta.get('turns_count', 0)}</code>\n\n"
|
||
f"{title_section}"
|
||
)
|
||
if last_resp:
|
||
text += f"\n📋 <b>Last AI Response:</b>\n<i>\"{escape_html(last_resp)}\"</i>\n"
|
||
|
||
keyboard = []
|
||
if not is_active:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🟢 سوییچ و فعالسازی این گفتگو" if is_fa else "🟢 Switch & Resume Chat", callback_data=f"conv_switch_{conv_id}"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🗑️ حذف این گفتگو" if is_fa else "🗑️ Delete this Conversation", callback_data=f"conv_del_ask_{conv_id}"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📜 بازگشت به لیست گفتگوها" if is_fa else "📜 Back to Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
def process_ai_schedule_actions(raw_text: str, chat_id: int, project_name: str, is_fa: bool) -> tuple[str, list[ScheduledTask]]:
|
||
"""
|
||
Scans AI response for schedule action tags:
|
||
[[SCHEDULE_TASK: timing="...", max_runs=..., type="...", title="...", content="..."]]
|
||
or
|
||
[[SCHEDULE: timing="...", max_runs=..., type="...", title="...", content="..."]]
|
||
|
||
Creates the tasks in task_scheduler and replaces the raw tags with formatted Telegram confirmation cards.
|
||
"""
|
||
created_tasks = []
|
||
|
||
pattern = r"\[\[(?:SCHEDULE_TASK|SCHEDULE|CREATE_TASK):\s*(.*?)\]\]"
|
||
|
||
def replacer(match):
|
||
attrs_str = match.group(1)
|
||
timing = ""
|
||
content = ""
|
||
title = ""
|
||
task_type = "prompt"
|
||
max_runs = None
|
||
|
||
kv_pairs = re.findall(r'(\w+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s,\]]+))', attrs_str)
|
||
for k, v1, v2, v3 in kv_pairs:
|
||
key = k.lower()
|
||
val = v1 if v1 != "" else (v2 if v2 != "" else v3)
|
||
if key == "timing":
|
||
timing = val
|
||
elif key in ("content", "prompt", "cmd", "command"):
|
||
content = val
|
||
elif key == "title":
|
||
title = val
|
||
elif key == "type":
|
||
task_type = val.lower()
|
||
elif key in ("max_runs", "repeat", "count", "times", "max_iterations"):
|
||
try:
|
||
if val and val.lower() not in ("none", "null", "unlimited", "0"):
|
||
max_runs = int(val)
|
||
except ValueError:
|
||
pass
|
||
|
||
if not timing or not content:
|
||
return ""
|
||
|
||
try:
|
||
task = task_scheduler.add_task(
|
||
chat_id=chat_id,
|
||
creator_id=chat_id,
|
||
project_name=project_name,
|
||
task_type=task_type,
|
||
content=content,
|
||
timing_spec=timing,
|
||
title=title,
|
||
max_runs=max_runs,
|
||
)
|
||
created_tasks.append(task)
|
||
|
||
type_name = "پرامپت هوش مصنوعی" if task_type == "prompt" else "دستور شل" if task_type == "command" else "یادآور"
|
||
next_rel = format_relative_time(task.next_run_timestamp - time.time(), is_fa=is_fa)
|
||
repeat_str = f"{task.max_runs} بار" if task.max_runs else "نامحدود (دائمی)"
|
||
|
||
if is_fa:
|
||
card = (
|
||
f"\n\n⏰ <b>تسک زمانبندی شده توسط هوش مصنوعی ثبت شد:</b>\n"
|
||
f"• 🏷️ <b>شناسه تسک:</b> <code>{task.id}</code>\n"
|
||
f"• ⏱️ <b>زمانبندی:</b> <code>{escape_html(task.timing_spec)}</code>\n"
|
||
f"• 🔄 <b>حداکثر تکرار:</b> <code>{repeat_str}</code>\n"
|
||
f"• 🛠 <b>نوع:</b> {type_name}\n"
|
||
f"• ⏳ <b>اولین اجرا:</b> {next_rel} (<code>{format_timestamp(task.next_run_timestamp, is_fa=True)}</code>)\n"
|
||
f"• 📝 <b>محتوا:</b> <code>{escape_html(task.content[:120])}</code>"
|
||
)
|
||
else:
|
||
card = (
|
||
f"\n\n⏰ <b>Scheduled Task Registered by AI:</b>\n"
|
||
f"• 🏷️ <b>Task ID:</b> <code>{task.id}</code>\n"
|
||
f"• ⏱️ <b>Timing:</b> <code>{escape_html(task.timing_spec)}</code>\n"
|
||
f"• 🔄 <b>Max Runs:</b> <code>{task.max_runs if task.max_runs else 'Unlimited'}</code>\n"
|
||
f"• 🛠 <b>Type:</b> {type_name}\n"
|
||
f"• ⏳ <b>First Run:</b> {next_rel} (<code>{format_timestamp(task.next_run_timestamp, is_fa=False)}</code>)\n"
|
||
f"• 📝 <b>Content:</b> <code>{escape_html(task.content[:120])}</code>"
|
||
)
|
||
return card
|
||
except Exception as e:
|
||
logger.error(f"Failed to auto-register AI schedule task: {e}")
|
||
return f"\n⚠️ <i>(خطا در ثبت زمانبندی: {escape_html(str(e))})</i>"
|
||
|
||
processed_text = re.sub(pattern, replacer, raw_text, flags=re.DOTALL)
|
||
return processed_text, created_tasks
|
||
|
||
|
||
def build_memory_menu(chat_id: int, view_type: str = "project", page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Builds interactive 3-tier AI Memory manager dashboard with tabs (project, user, global), pagination, and deletion."""
|
||
from memory_manager import memory_manager
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
is_admin_user = settings.is_admin(chat_id)
|
||
|
||
proj_name = curr_proj.name if curr_proj else "default"
|
||
|
||
# Non-admin users are strictly forbidden from viewing global memory
|
||
if view_type == "global" and not is_admin_user:
|
||
view_type = "project" if curr_proj else "user"
|
||
|
||
# Validate view_type
|
||
if view_type not in ("project", "user", "global"):
|
||
view_type = "project" if curr_proj else "user"
|
||
|
||
if view_type == "project":
|
||
memories = memory_manager.get_project_memories(user_id=chat_id, project_name=proj_name, limit=100)
|
||
elif view_type == "user":
|
||
memories = memory_manager.get_user_memories(user_id=chat_id, limit=100)
|
||
elif view_type == "global" and is_admin_user:
|
||
memories = memory_manager.get_global_memories(limit=100)
|
||
else:
|
||
memories = memory_manager.get_user_memories(user_id=chat_id, limit=100)
|
||
view_type = "user"
|
||
|
||
total_cnt = len(memories)
|
||
page_size = 5
|
||
max_pages = max(1, (total_cnt + page_size - 1) // page_size) if total_cnt > 0 else 1
|
||
page = max(0, min(page, max_pages - 1))
|
||
paged_memories = memories[page * page_size : (page + 1) * page_size]
|
||
|
||
stats = memory_manager.get_stats(user_id=chat_id, project_name=proj_name)
|
||
|
||
if is_fa:
|
||
if view_type == "project":
|
||
tab_name = f"📁 خاطرات اختصاصی پروژه (<code>{escape_html(proj_name)}</code>)"
|
||
tab_desc = "این خاطرات <b>فقط هنگام فعالیت در همین پروژه</b> به هوش مصنوعی تزریق میشوند."
|
||
elif view_type == "user":
|
||
tab_name = "👤 خاطرات شخصی من (سراسری)"
|
||
tab_desc = "این خاطرات مربوط به شخص شماست و در <b>تمام پروژهها</b> اعمال میگردد."
|
||
else:
|
||
tab_name = "🌐 قوانین و خاطرات عمومی (کل سرور و کاربران)"
|
||
tab_desc = "این قوانین برای <b>همه کاربران</b> تزریق میشوند (ویرایش و حذف فقط توسط مدیر)."
|
||
|
||
stats_str = f"<code>{stats['current_project_memories']}</code> پروژه | <code>{stats['current_user_memories']}</code> کاربر"
|
||
if is_admin_user:
|
||
stats_str += f" | <code>{stats['global_memories']}</code> عمومی"
|
||
|
||
header = (
|
||
f"🧠 <b>کنترلپنل حافظه هوشمند (AI Memory Dashboard)</b>\n\n"
|
||
f"• 📂 <b>بخش فعال:</b> {tab_name}\n"
|
||
f"• ℹ️ {tab_desc}\n"
|
||
f"• 📊 <b>آمار:</b> {stats_str}\n\n"
|
||
)
|
||
if not memories:
|
||
body = (
|
||
f"ℹ️ <i>در این بخش هنوز هیچ خاطرهای ثبت نشده است.</i>\n\n"
|
||
f"💡 <b>یادگیری خودکار:</b> هوش مصنوعی در حین گفتگوها نکات مهم مربوط به این بخش را خودکار استخراج و ثبت میکند."
|
||
)
|
||
else:
|
||
body_lines = [f"<b>لیست خاطرات (صفحه {page + 1} از {max_pages}):</b>\n"]
|
||
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
|
||
cat_badge = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||
body_lines.append(
|
||
f"<b>{idx}.</b> 🔑 <code>{escape_html(m.key)}</code> {cat_badge}\n"
|
||
f" 📝 {escape_html(m.content)}"
|
||
)
|
||
body = "\n\n".join(body_lines)
|
||
else:
|
||
if view_type == "project":
|
||
tab_name = f"📁 Project Memory (<code>{escape_html(proj_name)}</code>)"
|
||
tab_desc = "Injected only when working in this project."
|
||
elif view_type == "user":
|
||
tab_name = "👤 User Profile Memory"
|
||
tab_desc = "Applies to you across all your projects."
|
||
else:
|
||
tab_name = "🌐 Global System Memory"
|
||
tab_desc = "Applies to all users (admin-managed)."
|
||
|
||
stats_str = f"<code>{stats['current_project_memories']}</code> project | <code>{stats['current_user_memories']}</code> user"
|
||
if is_admin_user:
|
||
stats_str += f" | <code>{stats['global_memories']}</code> global"
|
||
|
||
header = (
|
||
f"🧠 <b>AI Memory Dashboard</b>\n\n"
|
||
f"• 📂 <b>Active Tab:</b> {tab_name}\n"
|
||
f"• ℹ️ {tab_desc}\n"
|
||
f"• 📊 <b>Stats:</b> {stats_str}\n\n"
|
||
)
|
||
if not memories:
|
||
body = "ℹ️ <i>No memories recorded in this section yet.</i>"
|
||
else:
|
||
body_lines = [f"<b>Memories (Page {page + 1} of {max_pages}):</b>\n"]
|
||
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
|
||
cat_badge = f"[{m.category}]" if m.category and m.category != "general" else ""
|
||
body_lines.append(
|
||
f"<b>{idx}.</b> 🔑 <code>{escape_html(m.key)}</code> {cat_badge}\n"
|
||
f" 📝 {escape_html(m.content)}"
|
||
)
|
||
body = "\n\n".join(body_lines)
|
||
|
||
text = f"{header}{body}"
|
||
|
||
# Build keyboard
|
||
keyboard = []
|
||
|
||
# 1. Tab buttons (Global tab only visible to admin)
|
||
t_proj = f"📁 پروژه ({proj_name[:10]}) ✓" if view_type == "project" else f"📁 پروژه ({proj_name[:10]})"
|
||
t_user = "👤 کاربر (من) ✓" if view_type == "user" else "👤 کاربر (من)"
|
||
t_glob = "🌐 عمومی ✓" if view_type == "global" else "🌐 عمومی"
|
||
if not is_fa:
|
||
t_proj = f"📁 Project ✓" if view_type == "project" else f"📁 Project"
|
||
t_user = "👤 User ✓" if view_type == "user" else "👤 User"
|
||
t_glob = "🌐 Global ✓" if view_type == "global" else "🌐 Global"
|
||
|
||
tab_row = [
|
||
InlineKeyboardButton(t_proj, callback_data="mem_tab:project:0"),
|
||
InlineKeyboardButton(t_user, callback_data="mem_tab:user:0"),
|
||
]
|
||
if is_admin_user:
|
||
tab_row.append(InlineKeyboardButton(t_glob, callback_data="mem_tab:global:0"))
|
||
keyboard.append(tab_row)
|
||
|
||
# 2. Item delete buttons
|
||
can_delete_current = (view_type in ("project", "user")) or (view_type == "global" and is_admin_user)
|
||
if can_delete_current and paged_memories:
|
||
del_row = []
|
||
for idx, m in enumerate(paged_memories, start=page * page_size + 1):
|
||
del_row.append(InlineKeyboardButton(f"🗑️ #{idx}", callback_data=f"mem_del_conf:{m.id}:{view_type}:{page}"))
|
||
if len(del_row) == 5:
|
||
keyboard.append(del_row)
|
||
del_row = []
|
||
if del_row:
|
||
keyboard.append(del_row)
|
||
|
||
# 3. Pagination row
|
||
if max_pages > 1:
|
||
pag_row = []
|
||
if page > 0:
|
||
pag_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"mem_tab:{view_type}:{page - 1}"))
|
||
pag_row.append(InlineKeyboardButton(f"📄 {page + 1}/{max_pages}", callback_data="noop"))
|
||
if page < max_pages - 1:
|
||
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"mem_tab:{view_type}:{page + 1}"))
|
||
keyboard.append(pag_row)
|
||
|
||
# 4. Action buttons (Add, Clear, Refresh)
|
||
action_row = [
|
||
InlineKeyboardButton("➕ افزودن خاطره" if is_fa else "➕ Add Memory", callback_data=f"mem_add_menu:{view_type}"),
|
||
]
|
||
if can_delete_current and total_cnt > 0:
|
||
clear_label = "🧹 پاکسازی این بخش" if is_fa else "🧹 Clear this section"
|
||
action_row.append(InlineKeyboardButton(clear_label, callback_data=f"mem_clear_conf:{view_type}"))
|
||
|
||
action_row.append(InlineKeyboardButton("🔄 تازهسازی" if is_fa else "🔄 Refresh", callback_data=f"mem_tab:{view_type}:{page}"))
|
||
keyboard.append(action_row)
|
||
|
||
# 5. Main menu button
|
||
keyboard.append([
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
InlineKeyboardButton("⚙️ تنظیمات" if is_fa else "⚙️ Settings", callback_data="btn_settings_menu"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
MEMORY_PRESETS = {
|
||
"project": {
|
||
"php_caddy": {
|
||
"key": "php_caddy_stack",
|
||
"category": "tech_stack",
|
||
"content": "همیشه از PHP 8.x مدرن به عنوان زبان اصلی بکاند و سرور Caddy برای Reverse Proxy و مدیریت خودکار SSL دامنه msa.artacloud.ir استفاده شود.",
|
||
"content_en": "Always use modern PHP 8.x as backend and Caddy reverse proxy for msa.artacloud.ir domains with automatic SSL.",
|
||
},
|
||
"qa_tests": {
|
||
"key": "qa_and_verification",
|
||
"category": "rule",
|
||
"content": "قبل از اتمام کار، تمام کدها، مسیرها، سینتکس و سرویسها باید به طور کامل تست، عیبیابی و اعتبارسنجی شوند.",
|
||
"content_en": "Always test, verify syntax, and perform QA audit before marking tasks complete.",
|
||
},
|
||
"clean_code": {
|
||
"key": "clean_code_standards",
|
||
"category": "rule",
|
||
"content": "ساختار تمیز دایرکتوریها، تفکیک فایلها، کامنتهای معنادار و الگوهای خوانا رعایت شوند.",
|
||
"content_en": "Follow clean code architecture, proper folder structure, and meaningful documentation.",
|
||
},
|
||
},
|
||
"user": {
|
||
"lang_fa": {
|
||
"key": "preferred_language",
|
||
"category": "preference",
|
||
"content": "همیشه تمام مکالمات، توضیحات، و پیامها به زبان فارسی روان و دقیق ارائه شوند.",
|
||
"content_en": "Always communicate and explain in fluent Persian.",
|
||
},
|
||
"concise_style": {
|
||
"key": "response_style",
|
||
"category": "preference",
|
||
"content": "پاسخها خلاصه، سریع، دقیق، ساختاریافته و با بولتپوینتهای خوانا ارسال شوند.",
|
||
"content_en": "Responses should be concise, well-structured, and easy to read.",
|
||
},
|
||
"tz_tehran": {
|
||
"key": "preferred_timezone",
|
||
"category": "preference",
|
||
"content": "منطقه زمانی کاربر Asia/Tehran (ایران) است و زمانبندیها بر اساس این منطقه انجام شوند.",
|
||
"content_en": "User timezone is Asia/Tehran.",
|
||
},
|
||
"php_pref": {
|
||
"key": "preferred_tech_stack",
|
||
"category": "preference",
|
||
"content": "ترجیح زبان برنامهنویسی و پشته توسعه دهنده PHP مدرن (PHP 8.x) است.",
|
||
"content_en": "Developer prefers modern PHP 8.x tech stack.",
|
||
},
|
||
},
|
||
"global": {
|
||
"user_isolation": {
|
||
"key": "user_isolation_security",
|
||
"category": "rule",
|
||
"content": "کاربران عادی به جز مدیر فقط حق کار درون دایرکتوری اختصاصی پروژه خود (/root/projects/{user_id}/...) را دارند و هرگز نباید پرامپتها یا دادههای سیستمی به آنها افشا شود.",
|
||
"content_en": "Non-admin users are strictly isolated to their own project directories.",
|
||
},
|
||
"caddy_ssl": {
|
||
"key": "server_domain_caddy",
|
||
"category": "rule",
|
||
"content": "تمام سرویسهای وب و سابدامینهای *.msa.artacloud.ir باید با Caddy با SSL خودکار تنظیم و فعال شوند.",
|
||
"content_en": "All web services and subdomains are routed through Caddy with automatic SSL.",
|
||
},
|
||
"hierarchy_rule": {
|
||
"key": "precedence_hierarchy",
|
||
"category": "rule",
|
||
"content": "سلسلهمراتب قطعی اولویت: ۱. قوانین عمومی سیستم ۲. خاطرات و تصمیمات اختصاصی پروژه ۳. ترجیحات کلی کاربر.",
|
||
"content_en": "Strict precedence: 1. Global rules, 2. Project memory, 3. User preferences.",
|
||
},
|
||
},
|
||
}
|
||
|
||
|
||
def build_memory_add_menu(chat_id: int, view_type: str = "project") -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Builds interactive memory creation / template guide menu with quick presets and copyable command."""
|
||
from memory_manager import memory_manager
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
is_admin_user = settings.is_admin(chat_id)
|
||
|
||
proj_name = curr_proj.name if curr_proj else "default"
|
||
|
||
if view_type == "global" and not is_admin_user:
|
||
view_type = "project" if curr_proj else "user"
|
||
if view_type not in ("project", "user", "global"):
|
||
view_type = "project" if curr_proj else "user"
|
||
|
||
if is_fa:
|
||
type_labels = {
|
||
"project": f"📁 پروژه اختصاصی (<code>{escape_html(proj_name)}</code>)",
|
||
"user": "👤 ترجیحات و مشخصات شخصی من (سراسری)",
|
||
"global": "🌐 قوانین عمومی سیستم (کل سرور)",
|
||
}
|
||
text = (
|
||
f"🧠 <b>افزودن خاطره به حافظه هوش مصنوعی (Add Memory)</b>\n\n"
|
||
f"• 🎯 <b>بخش انتخابی:</b> {type_labels.get(view_type, view_type)}\n\n"
|
||
f"💡 <b>روشهای ثبت خاطره:</b>\n\n"
|
||
f"📌 <b>۱. ثبت سریع با الگوهای آماده (با یک کلیک):</b>\n"
|
||
f"با لمس هر یک از دکمههای زیر، خاطره استاندارد بلافاصله در این بخش ذخیره میشود.\n\n"
|
||
f"📌 <b>۲. ثبت دستی با دستور مستقیم (کپی و ویرایش):</b>\n"
|
||
f"<code>/memory add {view_type} <کلید> | <متن خاطره یا قانون></code>\n\n"
|
||
f"<b>مثال:</b>\n"
|
||
f"• <code>/memory add {view_type} code_style | کدهای پایتون و PHP به صورت ماژولار و تمیز نوشته شوند</code>\n\n"
|
||
f"📌 <b>۳. آموزش مستقیم در چت با هوش مصنوعی:</b>\n"
|
||
f"در حین گفتگو بنویسید: <i>«این نکته رو در حافظه ثبت کن: پورت دیتابیس ۵۴۳۲ است»</i> تا خودکار به خاطر بسپارد."
|
||
)
|
||
else:
|
||
type_labels = {
|
||
"project": f"📁 Active Project (<code>{escape_html(proj_name)}</code>)",
|
||
"user": "👤 Personal Profile / Preferences",
|
||
"global": "🌐 Global System Rules",
|
||
}
|
||
text = (
|
||
f"🧠 <b>Add AI Long-Term Memory</b>\n\n"
|
||
f"• 🎯 <b>Target Tier:</b> {type_labels.get(view_type, view_type)}\n\n"
|
||
f"💡 <b>Methods to Add Memory:</b>\n\n"
|
||
f"📌 <b>1. One-Click Quick Presets:</b>\n"
|
||
f"Click any button below to instantly save standard rules or preferences.\n\n"
|
||
f"📌 <b>2. Manual Command:</b>\n"
|
||
f"<code>/memory add {view_type} <key> | <content></code>\n\n"
|
||
f"<b>Example:</b>\n"
|
||
f"• <code>/memory add {view_type} code_style | Write clean modular code</code>\n\n"
|
||
f"📌 <b>3. Direct Chat with AI:</b>\n"
|
||
f"Simply tell AI in chat: <i>'Remember that PostgreSQL port is 5432'</i>."
|
||
)
|
||
|
||
keyboard = []
|
||
|
||
# 1. Tier selection tabs
|
||
t_proj = f"📁 پروژه ({proj_name[:10]}) ✓" if view_type == "project" else f"📁 پروژه ({proj_name[:10]})"
|
||
t_user = "👤 کاربر (من) ✓" if view_type == "user" else "👤 کاربر (من)"
|
||
t_glob = "🌐 عمومی ✓" if view_type == "global" else "🌐 عمومی"
|
||
if not is_fa:
|
||
t_proj = "📁 Project ✓" if view_type == "project" else "📁 Project"
|
||
t_user = "👤 User ✓" if view_type == "user" else "👤 User"
|
||
t_glob = "🌐 Global ✓" if view_type == "global" else "🌐 Global"
|
||
|
||
tab_row = [
|
||
InlineKeyboardButton(t_proj, callback_data="mem_add_tab:project"),
|
||
InlineKeyboardButton(t_user, callback_data="mem_add_tab:user"),
|
||
]
|
||
if is_admin_user:
|
||
tab_row.append(InlineKeyboardButton(t_glob, callback_data="mem_add_tab:global"))
|
||
keyboard.append(tab_row)
|
||
|
||
# 2. Preset buttons based on view_type
|
||
if view_type == "project":
|
||
if is_fa:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🐘 اولویت با PHP مدرن و Caddy", callback_data="mem_add_preset:project:php_caddy"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🛡️ اجرای تست و اعتبارسنجی کامل", callback_data="mem_add_preset:project:qa_tests"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📁 تمیزی و سازماندهی کدها", callback_data="mem_add_preset:project:clean_code"),
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🐘 Prefer Modern PHP & Caddy", callback_data="mem_add_preset:project:php_caddy"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🛡️ Strict QA Testing & Audit", callback_data="mem_add_preset:project:qa_tests"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📁 Clean Code & Standards", callback_data="mem_add_preset:project:clean_code"),
|
||
])
|
||
elif view_type == "user":
|
||
if is_fa:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🇮🇷 پاسخها همیشه فارسی روان باشد", callback_data="mem_add_preset:user:lang_fa"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📝 پاسخهای فشرده و ساختاریافته", callback_data="mem_add_preset:user:concise_style"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("⏰ منطقه زمانی Asia/Tehran", callback_data="mem_add_preset:user:tz_tehran"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🐘 ترجیح اولویت توسعه با PHP", callback_data="mem_add_preset:user:php_pref"),
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🇮🇷 Always Respond in Persian", callback_data="mem_add_preset:user:lang_fa"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📝 Concise & Structured Style", callback_data="mem_add_preset:user:concise_style"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("⏰ Timezone Asia/Tehran", callback_data="mem_add_preset:user:tz_tehran"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🐘 Prefer PHP Stack", callback_data="mem_add_preset:user:php_pref"),
|
||
])
|
||
elif view_type == "global" and is_admin_user:
|
||
if is_fa:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔒 قرنطینه و تفکیک امنیتی کاربران", callback_data="mem_add_preset:global:user_isolation"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 رورسپروکسی Caddy و SSL خودکار", callback_data="mem_add_preset:global:caddy_ssl"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("⚖️ رعایت سلسلهمراتب ۳ سطحی حافظه", callback_data="mem_add_preset:global:hierarchy_rule"),
|
||
])
|
||
else:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔒 User Isolation & Security", callback_data="mem_add_preset:global:user_isolation"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("🌐 Caddy SSL Reverse Proxy", callback_data="mem_add_preset:global:caddy_ssl"),
|
||
])
|
||
keyboard.append([
|
||
InlineKeyboardButton("⚖️ Strict Memory Precedence", callback_data="mem_add_preset:global:hierarchy_rule"),
|
||
])
|
||
|
||
# 3. Navigation row
|
||
keyboard.append([
|
||
InlineKeyboardButton("🔙 لیست خاطرات" if is_fa else "🔙 Memory List", callback_data=f"mem_tab:{view_type}:0"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
|
||
return text, InlineKeyboardMarkup(keyboard)
|
||
|
||
|
||
def build_tasks_menu(chat_id: int, page: int = 0) -> tuple[str, InlineKeyboardMarkup]:
|
||
"""Builds interactive Scheduled Tasks manager dashboard with pagination and status buttons."""
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
|
||
# Privacy: Strictly retrieve tasks owned by or created by this chat_id
|
||
all_user_tasks = task_scheduler.get_user_tasks(chat_id)
|
||
|
||
# Sort: active first (by next_run), then paused, then completed/failed (by last_run desc)
|
||
def sort_key(t: ScheduledTask):
|
||
status_rank = {"active": 0, "paused": 1, "completed": 2, "failed": 3}.get(t.status, 4)
|
||
ts = t.next_run_timestamp if t.status == "active" else -(t.last_run_timestamp or 0)
|
||
return (status_rank, ts)
|
||
|
||
all_user_tasks.sort(key=sort_key)
|
||
|
||
active_cnt = sum(1 for t in all_user_tasks if t.status == "active")
|
||
paused_cnt = sum(1 for t in all_user_tasks if t.status == "paused")
|
||
completed_cnt = sum(1 for t in all_user_tasks if t.status in ("completed", "failed"))
|
||
total_cnt = len(all_user_tasks)
|
||
|
||
page_size = 5
|
||
max_pages = max(1, (total_cnt + page_size - 1) // page_size) if total_cnt > 0 else 1
|
||
page = max(0, min(page, max_pages - 1))
|
||
|
||
start_idx = page * page_size
|
||
page_tasks = all_user_tasks[start_idx : start_idx + page_size]
|
||
|
||
if is_fa:
|
||
header = (
|
||
f"⏰ <b>مدیریت تسکهای زمانبندی شده (Scheduled Tasks)</b>\n\n"
|
||
f"• 📊 <b>وضعیت تسکها:</b> <code>{total_cnt} تسک</code> (🟢 {active_cnt} فعال | ⏸️ {paused_cnt} متوقف | ✅ {completed_cnt} پایانیافته)\n"
|
||
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'نامشخص')}</code>\n\n"
|
||
)
|
||
else:
|
||
header = (
|
||
f"⏰ <b>Scheduled Tasks Manager</b>\n\n"
|
||
f"• 📊 <b>Task Stats:</b> <code>{total_cnt}</code> (🟢 {active_cnt} Active | ⏸️ {paused_cnt} Paused | ✅ {completed_cnt} Completed)\n"
|
||
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name if curr_proj else 'None')}</code>\n\n"
|
||
)
|
||
|
||
body_lines = []
|
||
keyboard = []
|
||
|
||
if not all_user_tasks:
|
||
if is_fa:
|
||
body_lines.append("<i>هنوز هیچ تسک زمانبندی شدهای ثبت نکردهاید.</i>\n")
|
||
else:
|
||
body_lines.append("<i>No scheduled tasks registered yet.</i>\n")
|
||
else:
|
||
for idx, t in enumerate(page_tasks, start=start_idx + 1):
|
||
status_icon = "🟢" if t.status == "active" else "⏸️" if t.status == "paused" else "✅" if t.status == "completed" else "❌"
|
||
type_icon = "🧠" if t.task_type == "prompt" else "⚡" if t.task_type == "command" else "🔔"
|
||
|
||
title_clean = (t.title or t.content).replace("\n", " ").strip()
|
||
if len(title_clean) > 35:
|
||
title_clean = title_clean[:32] + "..."
|
||
|
||
next_str = ""
|
||
if t.status == "active":
|
||
diff = t.next_run_timestamp - time.time()
|
||
next_str = f"⏳ {format_relative_time(diff, is_fa=is_fa)}"
|
||
elif t.status == "paused":
|
||
next_str = "⏸️ متوقف" if is_fa else "⏸️ Paused"
|
||
else:
|
||
next_str = "✅ انجام شد" if is_fa else "✅ Done"
|
||
|
||
runs_badge = f" [تکرار: {t.total_runs}/{t.max_runs}]" if t.max_runs else ""
|
||
|
||
if is_fa:
|
||
body_lines.append(
|
||
f"{status_icon} <b>#{idx}</b> <code>{t.id}</code> ({type_icon} {escape_html(t.timing_spec)}{runs_badge})\n"
|
||
f" └ <b>پروژه:</b> <code>{escape_html(t.project_name)}</code> | {next_str}\n"
|
||
f" └ <i>«{escape_html(title_clean)}»</i>\n"
|
||
)
|
||
else:
|
||
body_lines.append(
|
||
f"{status_icon} <b>#{idx}</b> <code>{t.id}</code> ({type_icon} {escape_html(t.timing_spec)}{runs_badge})\n"
|
||
f" └ <b>Proj:</b> <code>{escape_html(t.project_name)}</code> | {next_str}\n"
|
||
f" └ <i>\"{escape_html(title_clean)}\"</i>\n"
|
||
)
|
||
|
||
# Button for this task
|
||
btn_label = f"{status_icon} #{idx} [{t.id}] {t.timing_spec}"
|
||
keyboard.append([
|
||
InlineKeyboardButton(btn_label[:38], callback_data=f"task_detail:{t.id}"),
|
||
InlineKeyboardButton("▶️", callback_data=f"task_run:{t.id}"),
|
||
InlineKeyboardButton("🗑️", callback_data=f"task_del:{t.id}"),
|
||
])
|
||
|
||
# Pagination row
|
||
if max_pages > 1:
|
||
pag_row = []
|
||
if page > 0:
|
||
pag_row.append(InlineKeyboardButton("⬅️ قبلی" if is_fa else "⬅️ Prev", callback_data=f"tasks_page:{page-1}"))
|
||
pag_row.append(InlineKeyboardButton(f"📄 {page+1}/{max_pages}", callback_data=f"tasks_page:{page}"))
|
||
if page < max_pages - 1:
|
||
pag_row.append(InlineKeyboardButton("بعدی ➡️" if is_fa else "Next ➡️", callback_data=f"tasks_page:{page+1}"))
|
||
keyboard.append(pag_row)
|
||
|
||
# Actions row
|
||
action_row = [
|
||
InlineKeyboardButton("➕ افزودن تسک جدید" if is_fa else "➕ Add New Task", callback_data="task_new_guide"),
|
||
InlineKeyboardButton("🔄 بروزرسانی" if is_fa else "🔄 Refresh", callback_data=f"tasks_page:{page}"),
|
||
]
|
||
keyboard.append(action_row)
|
||
|
||
if completed_cnt > 0:
|
||
keyboard.append([
|
||
InlineKeyboardButton("🧹 پاکسازی پایانیافتهها" if is_fa else "🧹 Clear Completed", callback_data="task_clear_completed"),
|
||
])
|
||
|
||
keyboard.append([
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
InlineKeyboardButton("🔙 بستن" if is_fa else "🔙 Close", callback_data="proj_close"),
|
||
])
|
||
|
||
if is_fa:
|
||
footer = (
|
||
f"\n💡 <i>دستور ایجاد سریع:</i>\n"
|
||
f"<code>/schedule <زمانبندی> | <پرامپت یا دستور></code>\n"
|
||
f"<b>مثال:</b> <code>/schedule every 1h (3 بار) | بررسی سلامت سرور</code>"
|
||
)
|
||
else:
|
||
footer = (
|
||
f"\n💡 <i>Quick create command:</i>\n"
|
||
f"<code>/schedule <timing> | <prompt or cmd></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 <زمانبندی [تعداد تکرار]> | <متن دستور یا پرامپت></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 && 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 <timing [repeats]> | <prompt or command></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 <user_id></code>\n"
|
||
f"• لغو دسترسی کاربر: <code>/uninvite <user_id></code>\n"
|
||
f"• ساخت لینک دعوت: <code>/invitelink</code>"
|
||
)
|
||
else:
|
||
footer = (
|
||
f"\n💡 <i>Admin text commands:</i>\n"
|
||
f"• Invite by ID: <code>/invite <user_id></code>\n"
|
||
f"• Revoke user: <code>/uninvite <user_id></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 <name></code> - ساخت و فعالسازی پروژه اختصاصی با پوشه و حافظه مستقل\n"
|
||
f"• <code>/switch <name></code> - سوییچ بین پروژهها\n"
|
||
f"• <code>/delproject <name></code> - حذف یک پروژه\n"
|
||
f"• <code>/renameproject <قدیم> <جدید></code> - تغییر نام پروژه\n"
|
||
f"• <code>/backup [نام]</code> - تهیه فایل Zip و ارسال بکاپ پروژه در پارتهای ۵۰ مگابایتی\n\n"
|
||
f"<b>🐙 کنترل نسخه و لغو تغییرات (Git & Undo):</b>\n"
|
||
f"• <code>/git</code> یا <code>/repo</code> - داشبورد مدیریت گیت و مشاهده وضعیت مخزن Gitea\n"
|
||
f"• <code>/sync</code> - همگامسازی سریع با مخزن (Pull & Push)\n"
|
||
f"• <code>/commit [پیام]</code> - ثبت و ارسال دستی کامیت\n"
|
||
f"• <code>/undo [hash]</code> یا <code>/revert</code> - لغو امن آخرین تغییر (HEAD) یا یک کامیت مشخص از گذشته\n\n"
|
||
f"<b>⏰ زمانبندی تسکها (Scheduled Tasks & Cron):</b>\n"
|
||
f"• <code>/tasks</code> یا <code>/schedule</code> - کنترلپنل تعاملی تسکهای زمانبندی شده\n"
|
||
f"• <code>/schedule <زمان> | <پرامپت/دستور></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 && npm test</code>\n"
|
||
f"• <code>/tasks list</code> - مشاهده لیست کامل تسکها\n"
|
||
f"• <code>/tasks run <id></code> - اجرای فوری و دستی یک تسک\n"
|
||
f"• <code>/tasks pause <id></code> / <code>/tasks resume <id></code> - توقف یا فعالسازی\n"
|
||
f"• <code>/tasks del <id></code> - حذف تسک زمانبندی شده\n\n"
|
||
f"<b>🤝 اشتراکگذاری پروژه (Project Sharing):</b>\n"
|
||
f"• <code>/share <user_id></code> - اشتراکگذاری پروژه فعال با کاربر دیگر\n"
|
||
f"• <code>/share <نام_پروژه> <user_id></code> - اشتراکگذاری پروژه خاص با کاربر\n"
|
||
f"• <code>/unshare <user_id></code> - لغو دسترسی کاربر به پروژه فعال\n"
|
||
f"• <code>/shared</code> - مشاهده لیست پروژههای اشتراکی\n\n"
|
||
f"<b>💬 مدیریت گفتگو و تاریخچه:</b>\n"
|
||
f"• <code>/conversations</code> - مشاهده لیست تعاملی، سوییچ و حذف گفتگوها\n"
|
||
f"• <code>/switchconv <شماره یا شناسه></code> - سوییچ سریع به گفتگوی خاص\n"
|
||
f"• <code>/delconv <شماره یا شناسه></code> - حذف گفتگوی خاص یا گفتگوی فعال\n"
|
||
f"• <code>/clearconvs</code> - پاکسازی و حذف تمام گفتگوهای پروژه فعال\n"
|
||
f"• <code>/lastconv</code> - باز کردن و بازیابی آخرین گفتگو / گفتگوی قبلی\n"
|
||
f"• <code>/new</code> یا <code>/reset</code> - شروع گفتگوی تازه (با امکان بازگشت)\n"
|
||
f"• <code>/status</code> - مشاهده وضعیت مدل، پروژه، حافظه سرور و دیسک\n"
|
||
f"• <code>/server</code> یا <code>/hardware</code> - وضعیت لحظهای رم (RAM)، سیپییو (CPU) و حافظه داخلی سرور\n"
|
||
f"• <code>/usage</code> یا <code>/quota</code> - استعلام لحظهای سهمیه و مصرف AGY\n"
|
||
f"• <code>/last</code> - مشاهده آخرین خروجی و ابزارهای اجرا شده\n"
|
||
f"• <code>/context</code> - نمایش درصد اشغال کانتکست و تعداد توکنها\n"
|
||
f"• <code>/cancel</code> - لغو پردازش یا دستور در حال اجرا\n"
|
||
f"• <code>/restart</code> - ریاستارت سرویس ربات\n\n"
|
||
f"<b>⚙️ تنظیمات:</b>\n"
|
||
f"• <code>/model</code> - انتخاب مدل هوش مصنوعی\n"
|
||
f"• <code>/lang [زبان]</code> - تغییر زبان پاسخدهی (مثلاً <code>/lang fa</code>)\n"
|
||
f"• <code>/effort [low|med|high]</code> - تنظیم عمق تفکر استدلالی\n"
|
||
f"<b>🧠 حافظه بلندمدت هوش مصنوعی (AI Memory):</b>\n"
|
||
f"• <code>/memory</code> - کنترلپنل تعاملی حافظه هوشمند، دکمههای افزودن، مشاهده و پاکسازی خاطرات\n"
|
||
f"• <code>/memory add [project|user|global] کلید | متن</code> - افزودن مستقیم خاطره جدید\n"
|
||
f"• <code>/memory global</code> - مشاهده قوانین و خاطرات عمومی کل سیستم\n"
|
||
f"• <code>/memory private</code> - مشاهده خاطرات اختصاصی و شخصی شما\n"
|
||
f"• <code>/memory clear</code> - پاکسازی خاطرات اختصاصی شما\n"
|
||
f"• <i>امکان افزودن با ۱ کلیک از طریق دکمه «➕ افزودن خاطره» در پنل حافظه و منوی پروژهها فراهم است.</i>\n\n"
|
||
f"<b>👑 مدیریت کاربران و دعوتها (ویژه مدیر):</b>\n"
|
||
f"• <code>/users</code> - پنل تعاملی مدیریت کاربران و لیست مجاز\n"
|
||
f"• <code>/invite <user_id></code> - دعوت و اعطای مستقیم دسترسی به کاربر\n"
|
||
f"• <code>/invitelink</code> یا <code>/invite link</code> - ساخت لینک دعوت جدید اختصاصی\n"
|
||
f"• <code>/uninvite <user_id></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 <name></code> - Create & activate an isolated project\n"
|
||
f"• <code>/switch <name></code> - Switch active project\n"
|
||
f"• <code>/delproject <name></code> - Delete a project\n"
|
||
f"• <code>/renameproject <old> <new></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 & Timings:</b>\n"
|
||
f"• <code>/tasks</code> or <code>/schedule</code> - Interactive Scheduled Tasks Manager\n"
|
||
f"• <code>/schedule <timing> | <prompt/cmd></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 && npm test</code>\n"
|
||
f"• <code>/tasks list</code> - List all tasks\n"
|
||
f"• <code>/tasks run <id></code> - Run a task immediately\n"
|
||
f"• <code>/tasks pause <id></code> / <code>/tasks resume <id></code> - Pause/Resume\n"
|
||
f"• <code>/tasks del <id></code> - Delete scheduled task\n\n"
|
||
f"<b>🤝 Project Sharing:</b>\n"
|
||
f"• <code>/share <user_id></code> - Share active project with a user\n"
|
||
f"• <code>/share <proj_name> <user_id></code> - Share specific project\n"
|
||
f"• <code>/unshare <user_id></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 <num|id></code> - Quick switch to a specific conversation\n"
|
||
f"• <code>/delconv <num|id></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 & Invitation Management:</b>\n"
|
||
f"• <code>/users</code> - Interactive User Whitelist & Invite Panel\n"
|
||
f"• <code>/invite <user_id></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 <user_id></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 <نام_پروژه></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 <project_name></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 <نام_قدیمی> <نام_جدید></code>" if is_fa else "Usage: <code>/renameproject <old_name> <new_name></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 <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>You don't have any projects yet!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></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 <user_id></code> (اشتراک پروژه فعال)\n"
|
||
"• <code>/share <نام_پروژه> <user_id></code>\n\n"
|
||
"<b>مثال:</b> <code>/share 123456789</code>"
|
||
if is_fa else
|
||
"ℹ️ <b>Share Command Usage:</b>\n\n"
|
||
"• <code>/share <user_id></code> (Active project)\n"
|
||
"• <code>/share <project_name> <user_id></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 <user_id></code>" if is_fa else "❌ Please provide a numeric user ID: <code>/unshare <user_id></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 <user_id></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 <نام_پروژه></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 <user_id></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 <project_name></code>"
|
||
|
||
await update.message.reply_html(text)
|
||
|
||
# Command: /last or /output
|
||
@check_auth
|
||
async def last_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
output_data = get_conversation_last_output(curr_proj.conversation_id) if curr_proj.conversation_id else {}
|
||
ai_text = output_data.get("text") or curr_proj.last_response
|
||
tools_used = output_data.get("tools", [])
|
||
|
||
if not ai_text and not tools_used:
|
||
msg = f"ℹ️ <i>هیچ خروجی یا پاسخی در پروژه <b>{escape_html(curr_proj.name)}</b> ثبت نشده است.</i>" if is_fa else f"ℹ️ <i>No previous AI output found for project <b>{escape_html(curr_proj.name)}</b>.</i>"
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
if ai_text and ai_text.strip():
|
||
header = f"📋 <b>آخرین پاسخ هوش مصنوعی (پروژه: {escape_html(curr_proj.name)}):</b>\n\n" if is_fa else f"📋 <b>Last AI Output (Project: {escape_html(curr_proj.name)}):</b>\n\n"
|
||
formatted = markdown_to_telegram_html(ai_text)
|
||
full_msg = header + formatted
|
||
else:
|
||
tools_header = f"🛠 <b>آخرین ابزارهای اجرا شده (پروژه: {escape_html(curr_proj.name)}):</b>\n" if is_fa else f"🛠 <b>Last executed tools (Project: {escape_html(curr_proj.name)}):</b>\n"
|
||
tools_list = "\n".join([f"• <code>{escape_html(t)}</code>" for t in tools_used[-8:]])
|
||
full_msg = tools_header + tools_list
|
||
|
||
chunks = split_message(full_msg, max_length=settings.max_message_length)
|
||
for chunk in chunks:
|
||
await update.message.reply_html(chunk, disable_web_page_preview=True)
|
||
|
||
async def graceful_bot_restart(delay_after_done: float = 1.5, max_wait: float = 180.0):
|
||
"""
|
||
Waits for all in-progress AI operations, agent tasks, and background processes
|
||
to finish completely before restarting the systemd bot service.
|
||
"""
|
||
logger.info("Graceful restart requested. Waiting for in-progress operations to finish...")
|
||
start_wait = time.time()
|
||
|
||
while time.time() - start_wait < max_wait:
|
||
in_progress = any(sess.turn_in_progress for sess in session_manager.sessions.values())
|
||
has_tasks = len(session_manager.active_tasks) > 0
|
||
has_procs = len(session_manager.active_procs) > 0
|
||
|
||
if not in_progress and not has_tasks and not has_procs:
|
||
logger.info("All active tasks completed. Executing graceful restart now.")
|
||
break
|
||
|
||
await asyncio.sleep(1.5)
|
||
|
||
await asyncio.sleep(delay_after_done)
|
||
os.system("systemctl restart --no-block agy-telegram-bot.service &")
|
||
|
||
# Command: /upload or /drop
|
||
@check_auth
|
||
async def upload_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ You have no active projects."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
p_name = curr_proj.name
|
||
token = create_upload_token(chat_id, p_name)
|
||
upload_url = get_upload_url(token)
|
||
|
||
if is_fa:
|
||
msg = (
|
||
f"📤 <b>صفحه آپلود فایل اختصاصی پروژه: <code>{escape_html(p_name)}</code></b>\n\n"
|
||
f"با این لینک میتوانید فایلهای حجیم و پروژههای خود (تا سقف ۲ گیگابایت) را بدون محدودیت تلگرام آپلود کنید:\n\n"
|
||
f"🔗 <b>لینک اختصاصی:</b>\n"
|
||
f"{upload_url}\n\n"
|
||
f"💡 <i>پشتیبانی از اکسترکت خودکار فایلهای ZIP/TAR مستقیم در پوشه پروژه!</i>"
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("🌐 ورود به صفحه آپلود", url=upload_url)],
|
||
[InlineKeyboardButton("📁 مدیریت پروژهها", callback_data="proj_menu")],
|
||
]
|
||
else:
|
||
msg = (
|
||
f"📤 <b>Web Upload Portal for <code>{escape_html(p_name)}</code></b>\n\n"
|
||
f"🔗 <b>Upload Link (up to 2GB):</b>\n"
|
||
f"{upload_url}"
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("🌐 Open Upload Portal", url=upload_url)],
|
||
[InlineKeyboardButton("📁 Projects", callback_data="proj_menu")],
|
||
]
|
||
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
|
||
|
||
async def process_agent_turn_by_chat_id(
|
||
app: Application,
|
||
chat_id: int,
|
||
prompt: str,
|
||
initial_status_text: Optional[str] = None,
|
||
):
|
||
"""Triggers an agent turn from external event (like Web Uploader or AI Button)."""
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
return
|
||
|
||
# Create dummy update/context wrapper or send initial status message
|
||
if initial_status_text:
|
||
initial_status = initial_status_text
|
||
else:
|
||
model_to_use = getattr(curr_proj, "model", None) or getattr(session, "model", None) or settings.default_model or "gemini-3.7-flash-auto"
|
||
effort_to_use = getattr(curr_proj, "reasoning_effort", None) or getattr(session, "reasoning_effort", None)
|
||
model_display = get_model_display_name(model_to_use, effort_to_use)
|
||
curr_cid = getattr(curr_proj, "conversation_id", None)
|
||
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
|
||
proj_label = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
|
||
status_lines = [f"🤔 <b>{escape_html(model_display)}</b>"]
|
||
if conv_title:
|
||
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
|
||
initial_status = f"{proj_label}\n\n" + "\n".join(status_lines)
|
||
status_msg = await app.bot.send_message(
|
||
chat_id=chat_id,
|
||
text=initial_status,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
reply_markup=get_stop_button(session.language),
|
||
)
|
||
|
||
class DummyUpdate:
|
||
def __init__(self, c_id, s_msg):
|
||
self.effective_chat = type("Chat", (), {"id": c_id})()
|
||
self.effective_user = type("User", (), {"id": c_id})()
|
||
self.effective_message = s_msg
|
||
self.message = s_msg
|
||
|
||
class DummyContext:
|
||
def __init__(self, application):
|
||
self.application = application
|
||
self.bot = application.bot
|
||
|
||
dummy_update = DummyUpdate(chat_id, status_msg)
|
||
dummy_context = DummyContext(app)
|
||
await process_agent_turn(dummy_update, dummy_context, prompt, status_msg_to_reuse=status_msg)
|
||
|
||
# Command: /restart
|
||
@check_auth
|
||
async def restart_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
if not settings.is_admin(user_id):
|
||
await update.message.reply_html("⛔ /restart is restricted to Bot Administrators.")
|
||
return
|
||
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
in_progress = any(sess.turn_in_progress for sess in session_manager.sessions.values()) or len(session_manager.active_tasks) > 0
|
||
if in_progress:
|
||
msg = (
|
||
"⏳ <b>عملیات هوش مصنوعی در حال اجرا است.</b>\n\n"
|
||
"ربات منتظر اتمام کامل فرآیند پردازش میماند و بلافاصله پس از تکمیل آن به صورت خودکار و بدون وقفه ریاستارت خواهد شد..."
|
||
if is_fa else
|
||
"⏳ <b>AI operations are in progress.</b>\n\n"
|
||
"Bot will wait for all active operations to complete, then restart gracefully..."
|
||
)
|
||
else:
|
||
msg = (
|
||
"🔄 <b>در حال راهاندازی مجدد سرویس ربات...</b>\n\nتغییرات در چند ثانیه آینده اعمال خواهند شد."
|
||
if is_fa else
|
||
"🔄 <b>Restarting AGY Bot service...</b>\n\nChanges will take effect in a few seconds."
|
||
)
|
||
|
||
await update.message.reply_html(msg)
|
||
asyncio.create_task(graceful_bot_restart())
|
||
|
||
# Command: /exec
|
||
@check_auth
|
||
async def exec_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
if not settings.is_admin(user_id):
|
||
await update.message.reply_html("⛔ /exec is restricted to Bot Administrators.")
|
||
return
|
||
|
||
cmd_text = " ".join(context.args) if context.args else ""
|
||
if not cmd_text:
|
||
await update.message.reply_html("Usage: <code>/exec <shell command></code>")
|
||
return
|
||
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
ws_cwd = curr_proj.workspace if curr_proj else settings.default_workspace
|
||
|
||
status_msg = await update.message.reply_html("⏳ Executing command...")
|
||
try:
|
||
proc = await asyncio.create_subprocess_shell(
|
||
cmd_text,
|
||
stdout=asyncio.subprocess.PIPE,
|
||
stderr=asyncio.subprocess.PIPE,
|
||
limit=100 * 1024 * 1024,
|
||
cwd=ws_cwd,
|
||
)
|
||
stdout, stderr = await proc.communicate()
|
||
out = stdout.decode("utf-8", errors="replace")
|
||
err = stderr.decode("utf-8", errors="replace")
|
||
combined = (out + ("\nSTDERR:\n" + err if err else "")).strip()
|
||
if not combined:
|
||
combined = "(No output, return code 0)"
|
||
|
||
escaped = escape_html(combined)
|
||
for chunk in split_message(f"🖥️ <b>Command Output:</b>\n<pre>{escaped}</pre>"):
|
||
await update.message.reply_html(chunk)
|
||
await status_msg.delete()
|
||
except Exception as e:
|
||
await status_msg.edit_text(f"❌ Execution failed: {e}")
|
||
# Helper: Usage & Stats Action Buttons
|
||
def get_usage_buttons(lang: str = "fa") -> InlineKeyboardMarkup:
|
||
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
restart_label = "🔄 گفتگوی جدید" if is_fa else "🔄 New Chat"
|
||
compact_label = "🗜️ فشردهسازی" if is_fa else "🗜️ Compact"
|
||
conv_label = "📜 سشنها / گفتگوها" if is_fa else "📜 Conversations"
|
||
last_conv_label = "⏮️ گفتگوی قبلی" if is_fa else "⏮️ Previous Chat"
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(compact_label, callback_data="btn_compact"),
|
||
InlineKeyboardButton(restart_label, callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton(last_conv_label, callback_data="btn_conv_last"),
|
||
InlineKeyboardButton(conv_label, callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
return InlineKeyboardMarkup(keyboard)
|
||
|
||
# Helper: Stop Action Button for active tasks
|
||
def get_stop_button(lang: str = "fa") -> InlineKeyboardMarkup:
|
||
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
stop_label = "🛑 توقف" if is_fa else "🛑 Stop"
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton(stop_label, callback_data="btn_stop"),
|
||
]
|
||
]
|
||
return InlineKeyboardMarkup(keyboard)
|
||
|
||
# Command: /new or /reset
|
||
@check_auth
|
||
async def new_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = (
|
||
"⚠️ <b>شما هنوز هیچ پروژهای ایجاد نکردهاید!</b>\n\n"
|
||
"لطفاً ابتدا با دستور <code>/newproject <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>No active project found!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></code>."
|
||
)
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
await session_manager.reset_session(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
|
||
if is_fa:
|
||
msg = (
|
||
f"🔄 <b>گفتگوی جدید آغاز شد!</b>\n\n"
|
||
f"حافظه و تاریخچه گفتگوی قبلی برای پروژه <code>{escape_html(curr_proj.name)}</code> بازنشانی گردید.\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
|
||
f"💡 <i>در صورت نیاز به بازگشت به گفتگوی قبلی، از دکمه زیر یا دستور <code>/lastconv</code> استفاده نمایید:</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("⏮️ باز کردن گفتگوی قبلی", callback_data="btn_conv_last"),
|
||
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
msg = (
|
||
f"🔄 <b>New Conversation Started!</b>\n\n"
|
||
f"Conversation context reset for project <code>{escape_html(curr_proj.name)}</code>.\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n\n"
|
||
f"💡 <i>To return to the previous conversation, use the button below or <code>/lastconv</code>:</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("⏮️ Reopen Previous Chat", callback_data="btn_conv_last"),
|
||
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /compact or /compress
|
||
@check_auth
|
||
async def compact_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ ابتدا با دستور <code>/newproject</code> یک پروژه بسازید." if is_fa else "⚠️ Please create a project first."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
toks = get_conversation_context_tokens(curr_proj.conversation_id)
|
||
curr_proj.last_context_length = toks
|
||
session.last_context_length = toks
|
||
session_manager.save()
|
||
|
||
model_lower = (curr_proj.model or "").lower()
|
||
if "3.1-pro" in model_lower or "pro" in model_lower:
|
||
max_ctx = 2_000_000
|
||
elif "claude" in model_lower:
|
||
max_ctx = 200_000
|
||
elif "gpt" in model_lower:
|
||
max_ctx = 128_000
|
||
else:
|
||
max_ctx = 1_000_000
|
||
|
||
pct = (toks / max_ctx) * 100 if max_ctx else 0
|
||
pct_str = f"{pct:.1f}%"
|
||
free_pct = f"{max(0.0, 100.0 - pct):.1f}%"
|
||
|
||
if is_fa:
|
||
msg = (
|
||
f"🗜️ <b>کانتکست با موفقیت فشرده و بهینهسازی شد!</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📥 <b>طول کانتکست فعال:</b> <code>{toks:,} توکن</code>\n"
|
||
f"• 🎯 <b>سقف پنجره:</b> <code>{max_ctx:,} توکن</code>\n"
|
||
f"• 📊 <b>میزان اشغال پنجره (CL):</b> <code>{pct_str}</code> (فضای آزاد: <code>{free_pct}</code>)\n\n"
|
||
f"✅ حافظه گفتگو بهینهسازی شد و برای ادامه آماده است."
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
|
||
InlineKeyboardButton("📜 تاریخچه گفتگوها", callback_data="btn_conv_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
msg = (
|
||
f"🗜️ <b>Context Compacted & Optimized!</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📥 <b>Active Context:</b> <code>{toks:,} tokens</code>\n"
|
||
f"• 🎯 <b>Max Window:</b> <code>{max_ctx:,} tokens</code>\n"
|
||
f"• 📊 <b>Usage (CL):</b> <code>{pct_str}</code> (Free: <code>{free_pct}</code>)\n\n"
|
||
f"✅ Context memory is optimized and ready."
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
|
||
InlineKeyboardButton("📜 Conversations", callback_data="btn_conv_menu"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /lastconv or /lastconversation or /openlast or /resume or /open_last
|
||
@check_auth
|
||
async def last_conversation_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
success, msg_text, conv_id, meta = session_manager.reopen_last_conversation(chat_id)
|
||
if not success:
|
||
await update.message.reply_html(msg_text)
|
||
return
|
||
|
||
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
|
||
turns = meta.get("turns_count", 0)
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"⏮️ <b>آخرین گفتگو با موفقیت بازیابی و باز شد!</b>\n\n"
|
||
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>شناسه مکالمه:</b> <code>{conv_id}</code>\n"
|
||
f"• 🔢 <b>تعداد نوبتها:</b> <code>{turns} نوبت</code>\n"
|
||
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>اکنون میتوانید پیام جدید خود را ارسال کنید تا دقیقاً در ادامه همین گفتگو پاسخ داده شود.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📊 وضعیت سیستم", callback_data="btn_status_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
text = (
|
||
f"⏮️ <b>Last Conversation Reopened Successfully!</b>\n\n"
|
||
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Conversation ID:</b> <code>{conv_id}</code>\n"
|
||
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
|
||
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>Send your next message to continue this conversation.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("📊 Status", callback_data="btn_status_menu"),
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
|
||
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /conversations or /convs or /history_conv or /dialogs or /chats
|
||
@check_auth
|
||
async def conversations_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
text, markup = build_conversations_menu(chat_id)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
|
||
# Command: /switchconv or /switch_conv or /openconv or /useconv or /selectconv
|
||
@check_auth
|
||
async def switch_conv_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
if not context.args:
|
||
text, markup = build_conversations_menu(chat_id)
|
||
msg_prompt = (
|
||
"💡 <i>لطفاً شناسه یا شماره گفتگو را مشخص کنید، یا از لیست زیر انتخاب نمایید:</i>\n\n"
|
||
"<b>مثال:</b> <code>/switchconv 1</code> یا <code>/switchconv 6c56254f</code>\n\n"
|
||
if is_fa else
|
||
"💡 <i>Please specify a conversation number or ID, or select from the list below:</i>\n\n"
|
||
"<b>Example:</b> <code>/switchconv 1</code> or <code>/switchconv 6c56254f</code>\n\n"
|
||
)
|
||
await update.message.reply_html(msg_prompt + text, reply_markup=markup)
|
||
return
|
||
|
||
target = context.args[0].strip()
|
||
success, msg_text, meta = session_manager.switch_conversation(chat_id, target)
|
||
if not success:
|
||
await update.message.reply_html(msg_text)
|
||
return
|
||
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
target_cid = (curr_proj.conversation_id if curr_proj else target) or target
|
||
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
|
||
turns = meta.get("turns_count", 0)
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"💬 <b>با موفقیت به گفتگو سوییچ کردید!</b>\n\n"
|
||
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'default')}</code>\n"
|
||
f"• 💬 <b>شناسه مکالمه فعال:</b> <code>{target_cid}</code>\n"
|
||
f"• 🔢 <b>تعداد نوبتها:</b> <code>{turns} نوبت</code>\n"
|
||
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>پیامهای بعدی شما در ادامه این گفتگو ارسال خواهند شد.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🗑️ حذف این گفتگو", callback_data=f"conv_del_ask_{target_cid}"),
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
text = (
|
||
f"💬 <b>Switched Conversation Successfully!</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Active Conversation:</b> <code>{target_cid}</code>\n"
|
||
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
|
||
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>Your next messages will continue in this conversation.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🗑️ Delete this Chat", callback_data=f"conv_del_ask_{target_cid}"),
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
|
||
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /delconv or /deleteconv or /del_conv or /delete_conv
|
||
@check_auth
|
||
async def delete_conv_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
if not context.args:
|
||
if not curr_proj.conversation_id:
|
||
msg = (
|
||
"⚠️ گفتگوی فعالی برای حذف وجود ندارد.\n\n"
|
||
"💡 <i>برای حذف گفتگوی خاص:</i> <code>/delconv <شماره یا شناسه></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 <number or ID></code>\n"
|
||
"List: <code>/conversations</code>"
|
||
)
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
target_cid = curr_proj.conversation_id
|
||
if is_fa:
|
||
text = (
|
||
f"⚠️ <b>تأیید حذف گفتگوی فعال:</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>شناسه گفتگو:</b> <code>{target_cid}</code>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید این گفتگو را حذف کنید؟"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🗑️ بله، حذف شود", callback_data=f"conv_del_confirm_{target_cid}"),
|
||
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
else:
|
||
text = (
|
||
f"⚠️ <b>Confirm Delete Active Conversation:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Conversation ID:</b> <code>{target_cid}</code>\n\n"
|
||
f"Are you sure you want to delete this conversation?"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🗑️ Yes, Delete", callback_data=f"conv_del_confirm_{target_cid}"),
|
||
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
return
|
||
|
||
target = context.args[0].strip()
|
||
success, msg_text = session_manager.delete_conversation(chat_id, target)
|
||
if not success:
|
||
await update.message.reply_html(msg_text)
|
||
return
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"{msg_text}\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>گفتگوی فعال فعلی:</b> <code>{curr_proj.conversation_id or '🟢 نشست تازه (آماده)'}</code>"
|
||
)
|
||
else:
|
||
text = (
|
||
f"{msg_text}\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Active Conversation:</b> <code>{curr_proj.conversation_id or '🟢 New Session (Ready)'}</code>"
|
||
)
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 تاریخچه گفتگوها" if is_fa else "📜 Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
]
|
||
]
|
||
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /settopic or /settitle or /convtitle
|
||
@check_auth
|
||
async def set_conv_title_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
if not context.args:
|
||
msg = (
|
||
"💡 <b>راهنمای تنظیم عنوان گفتگو:</b>\n\n"
|
||
"<code>/settopic <عنوان یا موضوع جدید></code>\n"
|
||
"یا برای گفتگوی خاص:\n"
|
||
"<code>/settopic <شماره یا شناسه گفتگو> <عنوان جدید></code>"
|
||
if is_fa else
|
||
"💡 <b>Set Conversation Topic:</b>\n\n"
|
||
"<code>/settopic <New Title></code>\n"
|
||
"Or for a specific conversation:\n"
|
||
"<code>/settopic <number or ID> <New Title></code>"
|
||
)
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
first_arg = context.args[0].strip()
|
||
# Check if first arg is conv index or id
|
||
if (first_arg.isdigit() or first_arg.startswith("#") or len(first_arg) > 20) and len(context.args) > 1:
|
||
target_cid = first_arg
|
||
new_title = " ".join(context.args[1:]).strip()
|
||
else:
|
||
target_cid = curr_proj.conversation_id
|
||
new_title = " ".join(context.args).strip()
|
||
|
||
ok, msg, matched_cid = session_manager.set_conversation_title(chat_id, title=new_title, conv_id=target_cid)
|
||
if not ok:
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 تاریخچه گفتگوها" if is_fa else "📜 Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
]
|
||
]
|
||
await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /clearconvs or /deleteallconvs
|
||
@check_auth
|
||
async def clear_convs_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
if is_fa:
|
||
text = (
|
||
f"⚠️ <b>تأیید پاکسازی تمام گفتگوها:</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید تمام تاریخچه گفتگوهای پروژه <code>{escape_html(curr_proj.name)}</code> را پاکسازی و حذف نمایید؟\n\n"
|
||
f"<i>این عملیات تمام لاگها و مکالمات قبلی این پروژه را حذف خواهد کرد و قابل بازگشت نیست.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 بله، حذف همه گفتگوها", callback_data="conv_clear_all_confirm"),
|
||
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
else:
|
||
text = (
|
||
f"⚠️ <b>Confirm Clear All Conversations:</b>\n\n"
|
||
f"Are you sure you want to permanently delete all conversation history for project <code>{escape_html(curr_proj.name)}</code>?\n\n"
|
||
f"<i>This action cannot be undone.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 Yes, Delete All", callback_data="conv_clear_all_confirm"),
|
||
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
# Command: /git or /repo
|
||
@check_auth
|
||
async def git_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
text, markup = await build_git_menu(chat_id)
|
||
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
# Command: /sync
|
||
@check_auth
|
||
async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
status_msg = await update.message.reply_html("🔄 <i>در حال همگامسازی با مخزن گیت (Gitea)...</i>" if is_fa else "🔄 <i>Syncing with Gitea repository...</i>")
|
||
ok, res_msg = await git_manager.git_sync(curr_proj.workspace, message="Manual sync via /sync", repo_name=curr_proj.name)
|
||
urls = git_manager.get_repo_urls(curr_proj.name)
|
||
|
||
if is_fa:
|
||
out = (
|
||
f"🔄 <b>گزارش همگامسازی مخزن گیت (Git Sync):</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"📋 <b>جزییات:</b>\n{res_msg}"
|
||
)
|
||
else:
|
||
out = (
|
||
f"🔄 <b>Git Sync Report:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"📋 <b>Details:</b>\n{res_msg}"
|
||
)
|
||
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True)
|
||
|
||
# Command: /commit
|
||
@check_auth
|
||
async def commit_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
commit_msg = " ".join(context.args).strip() if context.args else f"Manual commit via /commit for {curr_proj.name}"
|
||
status_msg = await update.message.reply_html("💾 <i>در حال ثبت کامیت و ارسال به Gitea...</i>" if is_fa else "💾 <i>Committing and pushing to Gitea...</i>")
|
||
ok, res_msg = await git_manager.git_commit_and_push(curr_proj.workspace, message=commit_msg, repo_name=curr_proj.name)
|
||
urls = git_manager.get_repo_urls(curr_proj.name)
|
||
|
||
if is_fa:
|
||
out = (
|
||
f"💾 <b>گزارش ثبت کامیت و پوش:</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"📝 {res_msg}"
|
||
)
|
||
else:
|
||
out = (
|
||
f"💾 <b>Commit & Push Report:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"📝 {res_msg}"
|
||
)
|
||
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True)
|
||
|
||
# Command: /undo or /revert
|
||
@check_auth
|
||
async def undo_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
commit_target = context.args[0].strip() if context.args else "HEAD"
|
||
status_msg = await update.message.reply_html("↩️ <i>در حال لغو تغییرات (Git Revert)...</i>" if is_fa else "↩️ <i>Reverting commit...</i>")
|
||
|
||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_target)
|
||
urls = git_manager.get_repo_urls(curr_proj.name)
|
||
|
||
buttons = [
|
||
[
|
||
InlineKeyboardButton("📜 تاریخچه کامیتها" if is_fa else "📜 Commit History", callback_data="git_hist:1"),
|
||
InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"),
|
||
]
|
||
]
|
||
|
||
if is_fa:
|
||
out = (
|
||
f"↩️ <b>گزارش لغو تغییرات (Git Undo / Revert):</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🎯 <b>کامیت هدف:</b> <code>{escape_html(commit_target)}</code>\n"
|
||
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"{res_msg}"
|
||
)
|
||
else:
|
||
out = (
|
||
f"↩️ <b>Git Undo / Revert Report:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🎯 <b>Target Commit:</b> <code>{escape_html(commit_target)}</code>\n"
|
||
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
|
||
f"{res_msg}"
|
||
)
|
||
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup(buttons))
|
||
|
||
# Command: /model
|
||
@check_auth
|
||
async def model_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = (
|
||
"⚠️ <b>شما هنوز هیچ پروژهای ایجاد نکردهاید!</b>\n\n"
|
||
"لطفاً ابتدا با دستور <code>/newproject <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>No active project found!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></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 <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>No active project found!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></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 <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>You have not created any projects yet!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></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 <نام></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 <name></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: /memory or /memories
|
||
@check_auth
|
||
async def memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
is_admin_user = settings.is_admin(chat_id)
|
||
from memory_manager import memory_manager
|
||
|
||
if not context.args:
|
||
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
subcmd = context.args[0].lower().strip()
|
||
|
||
if subcmd in ("project", "proj", "p"):
|
||
text, markup = build_memory_menu(chat_id, view_type="project", page=0)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("user", "u", "me", "profile", "private"):
|
||
text, markup = build_memory_menu(chat_id, view_type="user", page=0)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("global", "g", "public", "system"):
|
||
if not is_admin_user:
|
||
await update.message.reply_html(
|
||
"🚫 <b>خطای دسترسی: مشاهده و مدیریت حافظه عمومی سیستم فقط مختص مدیر است.</b>"
|
||
if is_fa
|
||
else "🚫 <b>Access Denied: Viewing global system memory is restricted to administrators.</b>"
|
||
)
|
||
return
|
||
text, markup = build_memory_menu(chat_id, view_type="global", page=0)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
# Add memory manually: /memory add <global|user|project> <key> | <content>
|
||
if subcmd in ("add", "set", "save") and len(context.args) > 2:
|
||
target_type = context.args[1].lower().strip()
|
||
if target_type not in ("global", "user", "project"):
|
||
target_type = "user"
|
||
rest = " ".join(context.args[1:]).strip()
|
||
else:
|
||
rest = " ".join(context.args[2:]).strip()
|
||
|
||
if target_type == "global" and not is_admin_user:
|
||
await update.message.reply_html("🚫 <b>خطای دسترسی: فقط مدیر سیستم میتواند خاطره عمومی ثبت کند.</b>" if is_fa else "🚫 <b>Access Denied.</b>")
|
||
return
|
||
|
||
if "|" in rest:
|
||
key_part, content_part = rest.split("|", 1)
|
||
else:
|
||
parts = rest.split(None, 1)
|
||
key_part = parts[0]
|
||
content_part = parts[1] if len(parts) > 1 else ""
|
||
|
||
key_clean = key_part.strip().lower()
|
||
content_clean = content_part.strip()
|
||
if not key_clean or not content_clean:
|
||
await update.message.reply_html(
|
||
"💡 <i>راهنمای افزودن دستی خاطره:</i>\n<code>/memory add [global|user|project] <key> | <محتوا></code>\n\n<b>مثال:</b>\n<code>/memory add global default_php | همیشه از PHP مدرن استفاده شود</code>"
|
||
if is_fa
|
||
else "💡 <i>Usage:</i> <code>/memory add [global|user|project] <key> | <content></code>"
|
||
)
|
||
return
|
||
|
||
p_name = curr_proj.name if (curr_proj and target_type == "project") else "default"
|
||
item, is_created = memory_manager.save_or_update(
|
||
type_=target_type,
|
||
key=key_clean,
|
||
content=content_clean,
|
||
user_id=chat_id,
|
||
project_name=p_name if target_type == "project" else None,
|
||
category="rule" if target_type == "global" else "preference",
|
||
importance=5 if target_type == "global" else 3,
|
||
created_by=chat_id,
|
||
)
|
||
msg = f"✅ <b>خاطره با موفقیت ثبت شد:</b>\n• 🏷️ بخش: {item.type}\n• 🔑 کلید: <code>{escape_html(item.key)}</code>\n• 📝 محتوا: {escape_html(item.content)}" if is_fa else f"✅ <b>Memory saved:</b> [{item.type}] {item.key}"
|
||
text, markup = build_memory_menu(chat_id, view_type=target_type, page=0)
|
||
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("clear", "clean", "purge"):
|
||
target_type = context.args[1].lower() if len(context.args) > 1 else "project"
|
||
if target_type not in ("global", "user", "project"):
|
||
target_type = "project" if curr_proj else "user"
|
||
p_name = curr_proj.name if curr_proj else "default"
|
||
try:
|
||
cnt = memory_manager.clear_memories(
|
||
type_=target_type, user_id=chat_id, project_name=p_name, is_admin=is_admin_user
|
||
)
|
||
type_label = f"پروژه {p_name}" if target_type == "project" else ("عمومی" if target_type == "global" else "شخصی کاربر")
|
||
msg = f"🧹 <b>تعداد {cnt} خاطره {type_label} با موفقیت پاکسازی شد.</b>" if is_fa else f"🧹 <b>{cnt} {target_type} memories cleared.</b>"
|
||
except PermissionError:
|
||
msg = "🚫 <b>خطای دسترسی: فقط ادمین مجاز به پاکسازی خاطرات عمومی است.</b>" if is_fa else "🚫 <b>Access denied.</b>"
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
text, markup = build_memory_menu(chat_id, view_type="project" if curr_proj else "user", page=0)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
|
||
|
||
# Command: /context or /tokens
|
||
@check_auth
|
||
async def context_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||
await update.message.reply_html(msg)
|
||
return
|
||
|
||
stats_usage = {
|
||
"input_tokens": curr_proj.last_context_length or 0,
|
||
"total_tokens": curr_proj.last_total_tokens or curr_proj.last_context_length or 0,
|
||
}
|
||
stats_html = format_context_stats(
|
||
usage=stats_usage if curr_proj.last_context_length else None,
|
||
duration=0.0,
|
||
model=curr_proj.model,
|
||
effort=curr_proj.effort,
|
||
lang=session.language,
|
||
project_name=curr_proj.name,
|
||
conversation_id=curr_proj.conversation_id,
|
||
)
|
||
if is_fa:
|
||
text = (
|
||
f"📏 <b>وضعیت کانتکست و مصرف توکنها:</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>مدل:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📥 <b>طول کانتکست فعلی:</b> <code>{curr_proj.last_context_length or 0:,} توکن</code>\n"
|
||
f"• 📊 <b>کل توکنهای مصرفی:</b> <code>{curr_proj.last_total_tokens or 0:,} توکن</code>\n\n"
|
||
f"🏷️ <b>نشانگر خلاصه:</b>\n{stats_html}"
|
||
)
|
||
else:
|
||
text = (
|
||
f"📏 <b>Context & Token Statistics:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 🧠 <b>Model:</b> <code>{curr_proj.model}</code>\n"
|
||
f"• 📥 <b>Current Context Length:</b> <code>{curr_proj.last_context_length or 0:,} tokens</code>\n"
|
||
f"• 📊 <b>Total Tokens Used:</b> <code>{curr_proj.last_total_tokens or 0:,} tokens</code>\n\n"
|
||
f"🏷️ <b>Badge Summary:</b>\n{stats_html}"
|
||
)
|
||
await update.message.reply_html(text, reply_markup=get_usage_buttons(session.language))
|
||
|
||
# Command: /cancel
|
||
@check_auth
|
||
async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
cancelled = session_manager.cancel_active_task(chat_id)
|
||
session.turn_in_progress = False
|
||
session_manager.save()
|
||
|
||
if cancelled:
|
||
msg = "🛑 <b>عملیات و پردازش فعلی لغو شد.</b>" if is_fa else "🛑 <b>Active process / task was cancelled.</b>"
|
||
else:
|
||
msg = "ℹ️ <i>هیچ پردازش فعالی در حال اجرا نبود.</i>" if is_fa else "ℹ️ <i>No active task is currently running.</i>"
|
||
await update.message.reply_html(msg)
|
||
|
||
# Command: /tasks or /schedule
|
||
@check_auth
|
||
async def tasks_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
|
||
if not context.args:
|
||
text, markup = build_tasks_menu(chat_id)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
subcmd = context.args[0].lower().strip()
|
||
raw_args = " ".join(context.args).strip()
|
||
|
||
if subcmd == "list":
|
||
text, markup = build_tasks_menu(chat_id)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("help", "guide"):
|
||
text, markup = build_task_add_guide(chat_id)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("clear", "clean", "purge"):
|
||
cnt = task_scheduler.clear_completed_tasks(chat_id)
|
||
msg = f"🧹 <b>{cnt} تسک پایانیافته پاکسازی شد.</b>" if is_fa else f"🧹 <b>{cnt} completed tasks cleared.</b>"
|
||
text, markup = build_tasks_menu(chat_id)
|
||
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
|
||
return
|
||
|
||
if subcmd in ("run", "exec", "start") and len(context.args) > 1:
|
||
task_id = context.args[1].strip()
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
|
||
return
|
||
status_msg = await update.message.reply_html(f"⏳ <b>در حال اجرای تسک <code>{task_id}</code>...</b>" if is_fa else f"⏳ <b>Executing task <code>{task_id}</code>...</b>")
|
||
success, out = await task_scheduler.execute_task_now(task_id, context.application)
|
||
try:
|
||
await status_msg.delete()
|
||
except Exception:
|
||
pass
|
||
return
|
||
|
||
if subcmd in ("pause", "stop") and len(context.args) > 1:
|
||
task_id = context.args[1].strip()
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
|
||
return
|
||
ok = task_scheduler.pause_task(task_id)
|
||
if ok:
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
await update.message.reply_html(f"⏸️ <b>تسک <code>{task_id}</code> متوقف شد.</b>\n\n{text}", reply_markup=markup)
|
||
else:
|
||
await update.message.reply_html("❌ <b>تسک یافت نشد یا از قبل متوقف است.</b>" if is_fa else "❌ <b>Task not found or already paused.</b>")
|
||
return
|
||
|
||
if subcmd in ("resume", "unpause", "play") and len(context.args) > 1:
|
||
task_id = context.args[1].strip()
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
|
||
return
|
||
ok = task_scheduler.resume_task(task_id)
|
||
if ok:
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
await update.message.reply_html(f"▶️ <b>تسک <code>{task_id}</code> فعال شد.</b>\n\n{text}", reply_markup=markup)
|
||
else:
|
||
await update.message.reply_html("❌ <b>تسک یافت نشد یا در حال حاضر فعال است.</b>" if is_fa else "❌ <b>Task not found or already active.</b>")
|
||
return
|
||
|
||
if subcmd in ("del", "delete", "remove", "rm") and len(context.args) > 1:
|
||
task_id = context.args[1].strip()
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await update.message.reply_html("❌ <b>تسک مورد نظر یافت نشد یا دسترسی مجاز نیست.</b>" if is_fa else "❌ <b>Task not found or access denied.</b>")
|
||
return
|
||
ok = task_scheduler.delete_task(task_id)
|
||
if ok:
|
||
text, markup = build_tasks_menu(chat_id)
|
||
await update.message.reply_html(f"🗑️ <b>تسک <code>{task_id}</code> با موفقیت حذف شد.</b>\n\n{text}", reply_markup=markup)
|
||
else:
|
||
await update.message.reply_html("❌ <b>تسک یافت نشد.</b>" if is_fa else "❌ <b>Task not found.</b>")
|
||
return
|
||
|
||
if subcmd in ("info", "view", "show") and len(context.args) > 1:
|
||
task_id = context.args[1].strip()
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
await update.message.reply_html(text, reply_markup=markup)
|
||
return
|
||
|
||
# Add task parsing: /schedule add <timing> | <content> OR /schedule <timing> | <content>
|
||
add_payload = raw_args
|
||
if add_payload.lower().startswith("add "):
|
||
add_payload = add_payload[4:].strip()
|
||
|
||
if "|" not in add_payload:
|
||
text, markup = build_task_add_guide(chat_id)
|
||
await update.message.reply_html(
|
||
f"⚠️ <b>فرمت دستور صحیح نیست!</b>\n\n"
|
||
f"لطفاً از علامت خط عمودی <code>|</code> برای جدا کردن زمانبندی و متن پرامپت استفاده کنید:\n"
|
||
f"<code>/schedule <زمانبندی [تعداد تکرار]> | <پرامپت یا دستور></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 <user_id></code>\n"
|
||
"• ساخت لینک دعوت: <code>/invitelink</code> یا <code>/invite link</code>"
|
||
if is_fa else
|
||
"⚠️ <b>Invalid format!</b>\n\n"
|
||
"• Invite by ID: <code>/invite <user_id></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 <user_id></code>" if is_fa else "Usage: <code>/uninvite <user_id></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.* > 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.* > 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 <password></code>")
|
||
return
|
||
|
||
password = context.args[0].strip()
|
||
if password == settings.auth_password:
|
||
settings.add_authorized_user(user_id)
|
||
await update.message.reply_html("🎉 <b>Authentication successful!</b> You now have access to the bot. Send /start to begin.")
|
||
else:
|
||
await update.message.reply_html("❌ <b>Incorrect password.</b> Access denied.")
|
||
|
||
# Callback Query Handler
|
||
async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
query = update.callback_query
|
||
user_id = update.effective_user.id
|
||
if not settings.is_user_authorized(user_id):
|
||
try:
|
||
await query.answer("⛔ دسترسی محدود است / Unauthorized", show_alert=True)
|
||
except Exception:
|
||
pass
|
||
return
|
||
|
||
chat_id = update.effective_chat.id
|
||
session = session_manager.get_or_create(chat_id)
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
is_admin_user = settings.is_admin(chat_id) or settings.is_admin(user_id)
|
||
data = query.data
|
||
|
||
if data == "btn_dashboard":
|
||
text, markup = build_main_dashboard(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_git_menu":
|
||
text, markup = await build_git_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data == "btn_git_sync":
|
||
await query.answer("🔄 در حال همگامسازی با Gitea..." if is_fa else "🔄 Syncing with Gitea...")
|
||
if curr_proj:
|
||
await git_manager.git_sync(curr_proj.workspace, message="Sync from bot UI", repo_name=curr_proj.name)
|
||
text, markup = await build_git_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data == "btn_git_commit":
|
||
await query.answer("💾 در حال ثبت کامیت و ارسال..." if is_fa else "💾 Committing and pushing...")
|
||
if curr_proj:
|
||
await git_manager.git_commit_and_push(curr_proj.workspace, message=f"Commit from bot UI for {curr_proj.name}", repo_name=curr_proj.name)
|
||
text, markup = await build_git_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data == "btn_git_pull":
|
||
await query.answer("⬇️ در حال دریافت آخرین تغییرات..." if is_fa else "⬇️ Pulling latest changes...")
|
||
if curr_proj:
|
||
await git_manager.git_pull(curr_proj.workspace)
|
||
text, markup = await build_git_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("git_hist:"):
|
||
page_str = data.split(":", 1)[1]
|
||
try:
|
||
page_num = int(page_str)
|
||
except ValueError:
|
||
page_num = 1
|
||
await query.answer("📜 در حال دریافت تاریخچه کامیتها..." if is_fa else "📜 Loading commit history...")
|
||
text, markup = await build_git_history_menu(chat_id, page=page_num)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("git_view:"):
|
||
commit_hash = data.split(":", 1)[1]
|
||
await query.answer("🔍 در حال بارگذاری مشخصات کامیت..." if is_fa else "🔍 Loading commit details...")
|
||
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("git_co:"):
|
||
target_ref = data.split(":", 1)[1]
|
||
await query.answer("🔀 در حال سوییچ به کامیت..." if is_fa else "🔀 Switching to commit...")
|
||
if curr_proj:
|
||
ok, co_msg = await git_manager.git_checkout(curr_proj.workspace, target_ref)
|
||
if ok:
|
||
await query.answer("✅ " + co_msg, show_alert=True)
|
||
else:
|
||
await query.answer("❌ " + co_msg, show_alert=True)
|
||
text, markup = await build_git_commit_detail_menu(chat_id, target_ref)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data == "git_co_main":
|
||
await query.answer("🌿 در حال بازگشت به شاخه اصلی (main)..." if is_fa else "🌿 Returning to main...")
|
||
if curr_proj:
|
||
ok, co_msg = await git_manager.git_checkout(curr_proj.workspace, "main")
|
||
if ok:
|
||
await query.answer("✅ سوییچ به شاخه اصلی (main) با موفقیت انجام شد." if is_fa else "✅ Switched to main branch.", show_alert=True)
|
||
else:
|
||
await query.answer("❌ " + co_msg, show_alert=True)
|
||
text, markup = await build_git_history_menu(chat_id, page=1)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("git_ask_reset:"):
|
||
commit_hash = data.split(":", 1)[1]
|
||
confirm_text = (
|
||
f"⚠️ <b>هشدار بازنشانی و بازگشت پروژه (Git Hard Reset)</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید وضعیت فایلها و پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> را دقیقاً به کامیت <code>{escape_html(commit_hash)}</code> بازگردانید؟\n\n"
|
||
f"<i>کلیه تغییرات بعد از این کامیت بازنشانی خواهند شد.</i>"
|
||
if is_fa else
|
||
f"⚠️ <b>Warning: Git Hard Reset</b>\n\n"
|
||
f"Are you sure you want to reset project <code>{escape_html(curr_proj.name if curr_proj else '')}</code> to commit <code>{escape_html(commit_hash)}</code>?\n\n"
|
||
f"<i>All uncommitted or subsequent changes will be reset.</i>"
|
||
)
|
||
confirm_markup = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton("✅ بله، بازگردانی شود" if is_fa else "✅ Yes, Reset to this commit", callback_data=f"git_do_reset:{commit_hash}"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data=f"git_view:{commit_hash}"),
|
||
],
|
||
])
|
||
try:
|
||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
|
||
except Exception:
|
||
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
|
||
|
||
elif data.startswith("git_do_reset:"):
|
||
commit_hash = data.split(":", 1)[1]
|
||
await query.answer("⏪ در حال بازگردانی به کامیت..." if is_fa else "⏪ Resetting to commit...")
|
||
if curr_proj:
|
||
ok, reset_msg = await git_manager.git_reset_hard(curr_proj.workspace, commit_hash)
|
||
if ok:
|
||
await query.answer("✅ بازگردانی با موفقیت انجام شد." if is_fa else "✅ Reset completed successfully.", show_alert=True)
|
||
else:
|
||
await query.answer("❌ " + reset_msg, show_alert=True)
|
||
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data == "git_ask_undo_head":
|
||
confirm_text = (
|
||
f"⚠️ <b>تأیید لغو آخرین تغییرات (Git Revert HEAD)</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید آخرین کامیت ثبتشده در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> را لغو کنید؟\n\n"
|
||
f"<i>یک کامیت معکوس ایجاد خواهد شد و تاریخچهٔ پروژه با امنیت کامل حفظ میشود.</i>"
|
||
if is_fa else
|
||
f"⚠️ <b>Confirm Undo Last Commit (Git Revert HEAD)</b>\n\n"
|
||
f"Are you sure you want to revert the latest commit on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
|
||
f"<i>A safe reverse commit will be created preserving your Git history.</i>"
|
||
)
|
||
confirm_markup = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton("↩️ بله، لغو شود (Revert)" if is_fa else "↩️ Yes, Revert HEAD", callback_data="git_do_undo_head"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data="btn_git_menu"),
|
||
],
|
||
])
|
||
try:
|
||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
|
||
except Exception:
|
||
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
|
||
|
||
elif data == "git_do_undo_head":
|
||
await query.answer("↩️ در حال لغو آخرین تغییرات..." if is_fa else "↩️ Reverting latest commit...")
|
||
if curr_proj:
|
||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target="HEAD")
|
||
if ok:
|
||
await query.answer("✅ آخرین تغییر با موفقیت لغو شد." if is_fa else "✅ Successfully reverted latest commit.", show_alert=True)
|
||
else:
|
||
await query.answer("❌ " + res_msg, show_alert=True)
|
||
text, markup = await build_git_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("git_ask_revert:"):
|
||
commit_hash = data.split(":", 1)[1]
|
||
confirm_text = (
|
||
f"⚠️ <b>تأیید لغو کامیت (Git Revert)</b>\n\n"
|
||
f"آیا میخواهید تغییرات اعمالشده در کامیت <code>{escape_html(commit_hash)}</code> را در پروژه <code>{escape_html(curr_proj.name if curr_proj else '')}</code> لغو کنید؟\n\n"
|
||
f"<i>تغییرات این کامیت بهصورت یک کامیت معکوس جدید ثبت خواهد شد.</i>"
|
||
if is_fa else
|
||
f"⚠️ <b>Confirm Git Revert</b>\n\n"
|
||
f"Are you sure you want to revert commit <code>{escape_html(commit_hash)}</code> on project <code>{escape_html(curr_proj.name if curr_proj else '')}</code>?\n\n"
|
||
f"<i>Changes from this commit will be inverted in a new commit.</i>"
|
||
)
|
||
confirm_markup = InlineKeyboardMarkup([
|
||
[
|
||
InlineKeyboardButton("↩️ بله، لغو شود" if is_fa else "↩️ Yes, Revert", callback_data=f"git_do_revert:{commit_hash}"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("❌ انصراف و بازگشت" if is_fa else "❌ Cancel", callback_data=f"git_view:{commit_hash}"),
|
||
],
|
||
])
|
||
try:
|
||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=confirm_markup)
|
||
except Exception:
|
||
await query.message.reply_html(confirm_text, reply_markup=confirm_markup)
|
||
|
||
elif data.startswith("git_do_revert:"):
|
||
commit_hash = data.split(":", 1)[1]
|
||
await query.answer("↩️ در حال لغو کامیت..." if is_fa else "↩️ Reverting commit...")
|
||
if curr_proj:
|
||
ok, res_msg, extra = await git_manager.git_revert(curr_proj.workspace, commit_target=commit_hash)
|
||
if ok:
|
||
await query.answer("✅ کامیت با موفقیت لغو شد." if is_fa else "✅ Commit reverted successfully.", show_alert=True)
|
||
else:
|
||
await query.answer("❌ " + res_msg, show_alert=True)
|
||
text, markup = await build_git_commit_detail_menu(chat_id, commit_hash)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||
|
||
elif data.startswith("aibtn:"):
|
||
btn_id = data.split(":", 1)[1].strip()
|
||
from bot_actions import get_ai_button_payload
|
||
payload_data = get_ai_button_payload(btn_id)
|
||
|
||
if not payload_data:
|
||
await query.answer("⚠️ این دکمه منقضی شده یا نامعتبر است." if is_fa else "⚠️ This button has expired or is invalid.", show_alert=True)
|
||
return
|
||
|
||
act_type = payload_data.get("type", "prompt")
|
||
act_value = payload_data.get("value", "").strip()
|
||
btn_project = payload_data.get("project")
|
||
|
||
if act_type == "prompt":
|
||
await query.answer("🧠 در حال ارسال دستور به هوش مصنوعی..." if is_fa else "🧠 Processing prompt with AI...")
|
||
if btn_project:
|
||
accessible = session_manager.get_all_accessible_projects(chat_id)
|
||
for k, p in accessible.items():
|
||
if k.lower() == btn_project.lower() or p.name.lower() == btn_project.lower():
|
||
session.active_project = p.name
|
||
session_manager.save()
|
||
break
|
||
|
||
prompt_echo = (
|
||
f"💬 <b>دستور ارسالی:</b> <code>{escape_html(act_value)}</code>"
|
||
if is_fa else
|
||
f"💬 <b>Sent prompt:</b> <code>{escape_html(act_value)}</code>"
|
||
)
|
||
try:
|
||
await query.message.reply_html(prompt_echo)
|
||
except Exception:
|
||
pass
|
||
|
||
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, act_value))
|
||
|
||
elif act_type == "cmd":
|
||
await query.answer("⚡ در حال اجرای دستور..." if is_fa else "⚡ Running command...")
|
||
cmd = act_value
|
||
if cmd.startswith("/"):
|
||
cmd_parts = cmd.split(maxsplit=1)
|
||
c_name = cmd_parts[0].lower()
|
||
if c_name in ("/usage", "/quota", "/credits", "/credit"):
|
||
text, markup = await build_usage_report(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/status", "/stats", "/sys"):
|
||
text, markup = build_server_hardware_menu(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/git", "/gitea"):
|
||
text, markup = await build_git_menu(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/tasks", "/cron", "/schedule"):
|
||
text, markup = build_tasks_menu(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/memory", "/mem"):
|
||
text, markup = build_memory_menu(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/projects", "/proj"):
|
||
text, markup = build_projects_menu(chat_id)
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
elif c_name in ("/backup", "/export"):
|
||
if curr_proj:
|
||
await query.message.reply_html("📦 در حال تهیه فایل پشتیبان..." if is_fa else "📦 Creating backup...")
|
||
zip_p = await backup_manager.create_project_backup(curr_proj.workspace, curr_proj.name)
|
||
if zip_p and zip_p.exists():
|
||
await context.bot.send_document(
|
||
chat_id=chat_id,
|
||
document=zip_p.open("rb"),
|
||
filename=zip_p.name,
|
||
caption=f"📦 Backup of <b>{escape_html(curr_proj.name)}</b>",
|
||
parse_mode=constants.ParseMode.HTML,
|
||
)
|
||
else:
|
||
await query.message.reply_html("❌ خطا در ایجاد پشتیبان." if is_fa else "❌ Failed to create backup.")
|
||
else:
|
||
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, cmd))
|
||
else:
|
||
asyncio.create_task(process_agent_turn_by_chat_id(context.application, chat_id, cmd))
|
||
|
||
elif act_type == "action":
|
||
query.data = act_value
|
||
await callback_handler(update, context)
|
||
return
|
||
|
||
elif data in ("btn_usage_menu", "btn_usage_refresh"):
|
||
await query.answer("⏳ استعلام مصرف..." if is_fa else "⏳ Refreshing...")
|
||
text, markup = await build_usage_report(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data in ("btn_hw_menu", "btn_hw_refresh"):
|
||
await query.answer("⏳ استعلام سختافزار سرور..." if is_fa else "⏳ Refreshing Hardware...")
|
||
text, markup = build_server_hardware_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_help_menu":
|
||
if is_fa:
|
||
text = (
|
||
f"📖 <b>راهنمای جامع ربات Antigravity (AGY)</b>\n\n"
|
||
f"<b>📁 مدیریت چند پروژهای (Projects):</b>\n"
|
||
f"• <code>/projects</code> یا <code>/project</code> - پنل تعاملی مدیریت و انتخاب پروژهها\n"
|
||
f"• <code>/newproject <name></code> - ساخت و فعالسازی پروژه اختصاصی\n"
|
||
f"• <code>/switch <name></code> - سوییچ بین پروژهها\n"
|
||
f"• <code>/delproject <name></code> - حذف یک پروژه\n\n"
|
||
f"<b>🤝 اشتراکگذاری پروژه (Project Sharing):</b>\n"
|
||
f"• <code>/share <user_id></code> - اشتراکگذاری پروژه فعال\n"
|
||
f"• <code>/unshare <user_id></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 <name></code> - Create & activate project\n"
|
||
f"• <code>/switch <name></code> - Switch active project\n\n"
|
||
f"<b>🤝 Sharing:</b>\n"
|
||
f"• <code>/share <user_id></code> - Share active project\n"
|
||
f"• <code>/unshare <user_id></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
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
await query.answer("✅ گفتگو با موفقیت تغییر یافت.")
|
||
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
|
||
turns = meta.get("turns_count", 0)
|
||
if is_fa:
|
||
text = (
|
||
f"💬 <b>گفتگو تغییر یافت!</b>\n\n"
|
||
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name if curr_proj else 'default')}</code>\n"
|
||
f"• 💬 <b>شناسه مکالمه فعال:</b> <code>{target_cid}</code>\n"
|
||
f"• 🔢 <b>تعداد نوبتها:</b> <code>{turns} نوبت</code>\n"
|
||
f"• 📝 <b>موضوع/اولین پیام:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>پیامهای بعدی شما در این گفتگو ارسال خواهند شد.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 لیست همه گفتگوها", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 گفتگوی جدید", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🗑️ حذف این گفتگو", callback_data=f"conv_del_ask_{target_cid}"),
|
||
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
else:
|
||
text = (
|
||
f"💬 <b>Conversation Switched!</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>Active Conversation:</b> <code>{target_cid}</code>\n"
|
||
f"• 🔢 <b>Turns:</b> <code>{turns}</code>\n"
|
||
f"• 📝 <b>First Prompt:</b> <i>{escape_html(first_prompt[:120])}</i>\n\n"
|
||
f"💡 <i>Your next messages will continue in this conversation.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📜 All Conversations", callback_data="btn_conv_menu"),
|
||
InlineKeyboardButton("🔄 New Chat", callback_data="btn_restart"),
|
||
],
|
||
[
|
||
InlineKeyboardButton("🗑️ Delete this Chat", callback_data=f"conv_del_ask_{target_cid}"),
|
||
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||
],
|
||
]
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("conv_view_"):
|
||
target_cid = data[len("conv_view_"):]
|
||
if not curr_proj:
|
||
await query.answer("⚠️ ابتدا پروژهای بسازید.", show_alert=True)
|
||
return
|
||
text, markup = build_conversation_detail_menu(chat_id, target_cid)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("conv_del_ask_"):
|
||
target_cid = data[len("conv_del_ask_"):]
|
||
if not curr_proj:
|
||
await query.answer("⚠️ ابتدا پروژهای بسازید.", show_alert=True)
|
||
return
|
||
meta = get_conversation_metadata(target_cid)
|
||
first_prompt = meta.get("first_prompt") or "(بدون متن)"
|
||
prompt_snippet = first_prompt.replace("\n", " ").strip()[:60]
|
||
if is_fa:
|
||
text = (
|
||
f"⚠️ <b>تأیید حذف گفتگو:</b>\n\n"
|
||
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>شناسه:</b> <code>{target_cid}</code>\n"
|
||
f"• 📝 <b>موضوع:</b> <i>«{escape_html(prompt_snippet)}»</i>\n\n"
|
||
f"آیا از حذف کامل این گفتگو و پاکسازی اطلاعات آن مطمئن هستید؟"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🗑️ بله، حذف شود", callback_data=f"conv_del_confirm_{target_cid}"),
|
||
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
else:
|
||
text = (
|
||
f"⚠️ <b>Confirm Conversation Deletion:</b>\n\n"
|
||
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
|
||
f"• 💬 <b>ID:</b> <code>{target_cid}</code>\n"
|
||
f"• 📝 <b>Topic:</b> <i>\"{escape_html(prompt_snippet)}\"</i>\n\n"
|
||
f"Are you sure you want to permanently delete this conversation?"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🗑️ Yes, Delete", callback_data=f"conv_del_confirm_{target_cid}"),
|
||
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("conv_del_confirm_"):
|
||
target_cid = data[len("conv_del_confirm_"):]
|
||
if not curr_proj:
|
||
await query.answer("⚠️ ابتدا پروژهای بسازید.", show_alert=True)
|
||
return
|
||
success, msg = session_manager.delete_conversation(chat_id, target_cid)
|
||
if success:
|
||
await query.answer("✅ گفتگو حذف شد." if is_fa else "✅ Conversation deleted.", show_alert=True)
|
||
else:
|
||
await query.answer(strip_ansi(msg)[:100], show_alert=True)
|
||
text, markup = build_conversations_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "conv_clear_all_ask":
|
||
if not curr_proj:
|
||
await query.answer("⚠️ ابتدا پروژهای بسازید.", show_alert=True)
|
||
return
|
||
if is_fa:
|
||
text = (
|
||
f"⚠️ <b>تأیید پاکسازی تمام گفتگوها:</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید تمام تاریخچه گفتگوهای پروژه <code>{escape_html(curr_proj.name)}</code> را حذف کنید؟\n\n"
|
||
f"<i>این عملیات تمام مکالمات قبلی این پروژه را پاک کرده و غیرقابل بازگشت است.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 بله، حذف همه گفتگوها", callback_data="conv_clear_all_confirm"),
|
||
InlineKeyboardButton("❌ انصراف", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
else:
|
||
text = (
|
||
f"⚠️ <b>Confirm Clear All Conversations:</b>\n\n"
|
||
f"Are you sure you want to delete all conversations for project <code>{escape_html(curr_proj.name)}</code>?\n\n"
|
||
f"<i>This action is permanent and cannot be undone.</i>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 Yes, Delete All", callback_data="conv_clear_all_confirm"),
|
||
InlineKeyboardButton("❌ Cancel", callback_data="btn_conv_menu"),
|
||
]
|
||
]
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data == "conv_clear_all_confirm":
|
||
if not curr_proj:
|
||
await query.answer("⚠️ ابتدا پروژهای بسازید.", show_alert=True)
|
||
return
|
||
success, msg, count = session_manager.clear_project_conversations(chat_id)
|
||
if success:
|
||
await query.answer(f"✅ {count} گفتگو حذف شد." if is_fa else f"✅ {count} conversations deleted.", show_alert=True)
|
||
else:
|
||
await query.answer(strip_ansi(msg)[:100], show_alert=True)
|
||
text, markup = build_conversations_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_compress":
|
||
if not curr_proj:
|
||
await query.answer("⚠️ No active project.", show_alert=True)
|
||
return
|
||
if session.turn_in_progress:
|
||
await query.answer("⚠️ A task is currently running.", show_alert=True)
|
||
return
|
||
|
||
if not curr_proj.conversation_id and not curr_proj.last_context_length:
|
||
await query.answer("ℹ️ No conversation history to compress yet.", show_alert=True)
|
||
return
|
||
|
||
await query.answer("🗜️ Compressing context...")
|
||
compress_prompt = (
|
||
"Please compress and summarize our entire conversation context and session history into a concise structured memory summary. "
|
||
"Retain all critical facts, technical decisions, modified files, active tasks, and code changes, "
|
||
"while pruning redundant conversation history to optimize context length."
|
||
)
|
||
await process_agent_turn(update, context, compress_prompt)
|
||
|
||
# --- Project Callbacks ---
|
||
elif data == "proj_menu":
|
||
text, markup = build_projects_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("proj_switch:"):
|
||
target_name = data.split(":", 1)[1]
|
||
proj = await session_manager.switch_project(chat_id, target_name)
|
||
if proj:
|
||
await query.answer(f"Switched to: {proj.name}")
|
||
else:
|
||
await query.answer("Project not found", show_alert=True)
|
||
text, markup = build_projects_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
pass
|
||
|
||
elif data == "proj_new":
|
||
if is_fa:
|
||
msg = (
|
||
"ℹ️ <b>ساخت پروژه جدید:</b>\n\n"
|
||
"دستور ساخت پروژه را در چت ارسال کنید:\n"
|
||
"<code>/newproject <نام_پروژه></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 <project_name></code>\n\n"
|
||
"<b>Example:</b>\n"
|
||
"• <code>/newproject my-project</code>\n"
|
||
"• <code>/newproject webapp</code>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📁 پروژهها" if is_fa else "📁 Projects", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
|
||
]
|
||
]
|
||
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data == "proj_share_menu":
|
||
text, markup = build_sharing_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("proj_unshare_confirm:"):
|
||
target_uid_str = data.split(":", 1)[1]
|
||
if target_uid_str.isdigit():
|
||
success, msg, _ = await session_manager.unshare_project(chat_id, int(target_uid_str))
|
||
await query.answer(strip_ansi(msg)[:100])
|
||
text, markup = build_sharing_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
pass
|
||
|
||
elif data == "proj_del_menu":
|
||
own_projs = session_manager.get_user_projects(chat_id)
|
||
if not own_projs:
|
||
await query.answer("❌ شما پروژهای برای حذف ندارید.", show_alert=True)
|
||
return
|
||
keyboard = []
|
||
for name in own_projs.keys():
|
||
lbl = f"🗑️ حذف {name}" if is_fa else f"🗑️ Delete {name}"
|
||
keyboard.append([InlineKeyboardButton(lbl, callback_data=f"proj_del_confirm:{name}")])
|
||
keyboard.append([InlineKeyboardButton("🔙 بازگشت" if is_fa else "🔙 Back", callback_data="proj_menu")])
|
||
title = "🗑️ <b>پروژهای که میخواهید حذف شود را انتخاب کنید:</b>" if is_fa else "🗑️ <b>Select project to delete:</b>"
|
||
await query.edit_message_text(title, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("proj_del_confirm:"):
|
||
target_name = data.split(":", 1)[1]
|
||
text = (
|
||
f"⚠️ <b>تأیید حذف کامل و هوشمند پروژه:</b> <code>{escape_html(target_name)}</code>\n\n"
|
||
f"آیا از حذف کامل این پروژه و آزادسازی کلیه منابع آن اطمینان دارید؟\n\n"
|
||
f"<b>منابعی که بررسی و پاکسازی میشوند:</b>\n"
|
||
f"• 📁 کلیه فایلها و دایرکتوریهای پروژه در سرور\n"
|
||
f"• 🐙 مخزن اختصاصی در سرور گیت (Gitea)\n"
|
||
f"• 🌐 سابدامینها و رورسپراکسیهای متصل در Caddy\n"
|
||
f"• ⏰ تسکها و کرانجابهای زمانبندیشده مربوط به این پروژه\n"
|
||
f"• 🛑 فرآیندها، پورتها یا سرویسهای پسزمینه فعال"
|
||
if is_fa else
|
||
f"⚠️ <b>Confirm Full Teardown & Deletion:</b> <code>{escape_html(target_name)}</code>\n\n"
|
||
f"Are you sure you want to permanently delete this project and release all its resources?\n\n"
|
||
f"<b>Resources to be cleaned up:</b>\n"
|
||
f"• 📁 Workspace files and project directory\n"
|
||
f"• 🐙 Dedicated repository on Gitea server\n"
|
||
f"• 🌐 Subdomains & Caddy reverse proxies\n"
|
||
f"• ⏰ Scheduled tasks and cron jobs for this project\n"
|
||
f"• 🛑 Running processes, ports and services"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 بله، حذف و پاکسازی کامل" if is_fa else "💥 Yes, Full Teardown", callback_data=f"proj_del_execute:{target_name}"),
|
||
InlineKeyboardButton("❌ خیر، انصراف" if is_fa else "❌ No, Cancel", callback_data="proj_menu"),
|
||
]
|
||
]
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("proj_del_execute:"):
|
||
target_name = data.split(":", 1)[1]
|
||
await query.answer("⏳ در حال پاکسازی و آزادسازی کامل منابع..." if is_fa else "⏳ Releasing project resources...")
|
||
success, msg = await session_manager.delete_project(chat_id, target_name, delete_files=True)
|
||
keyboard = [
|
||
[InlineKeyboardButton("📁 مدیریت پروژهها" if is_fa else "📁 Projects", callback_data="proj_menu")],
|
||
[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")],
|
||
]
|
||
try:
|
||
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data == "proj_ws_info":
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
if not curr_proj:
|
||
await query.answer("پروژهای فعال نیست.", show_alert=True)
|
||
return
|
||
if is_fa:
|
||
msg = (
|
||
f"📂 <b>مسیر کاری پروژه {escape_html(curr_proj.name)}:</b>\n"
|
||
f"<code>{curr_proj.workspace}</code>\n\n"
|
||
f"<i>برای تغییر مسیر این پروژه دستور زیر را ارسال کنید:</i>\n"
|
||
f"<code>/workspace /path/to/project</code>"
|
||
)
|
||
else:
|
||
msg = (
|
||
f"📂 <b>Workspace for {escape_html(curr_proj.name)}:</b>\n"
|
||
f"<code>{curr_proj.workspace}</code>\n\n"
|
||
f"<i>To change workspace, send:</i>\n"
|
||
f"<code>/workspace /path/to/project</code>"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("📁 پروژهها" if is_fa else "📁 Projects", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
|
||
]
|
||
]
|
||
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("proj_upload_link:"):
|
||
target_name = data.split(":", 1)[1]
|
||
await query.answer("🌐 دریافت لینک اختصاصی آپلود..." if is_fa else "🌐 Generating upload link...")
|
||
accessible = session_manager.get_all_accessible_projects(chat_id)
|
||
proj = accessible.get(target_name)
|
||
if not proj:
|
||
for k, p in accessible.items():
|
||
if p.name == target_name:
|
||
proj = p
|
||
break
|
||
if not proj:
|
||
proj = session_manager.get_current_project(chat_id)
|
||
|
||
p_name = proj.name if proj else target_name
|
||
token = create_upload_token(chat_id, p_name)
|
||
upload_url = get_upload_url(token)
|
||
|
||
if is_fa:
|
||
msg = (
|
||
f"📤 <b>پنل اختصاصی آپلود فایل در پروژه <code>{escape_html(p_name)}</code></b>\n\n"
|
||
f"از طریق صفحه وب زیر میتوانید هرگونه فایل، پروژه، سورس کد یا فایلهای فشرده (ZIP) حجیم (بدون محدودیت ۲۰ مگابایت تلگرام تا سقف ۲ گیگابایت) را مستقیماً در مسیر پروژه آپلود کنید:\n\n"
|
||
f"🔗 <b>لینک اختصاصی و امن آپلود:</b>\n"
|
||
f"{upload_url}\n\n"
|
||
f"💡 <i>امکانات صفحه آپلود:</i>\n"
|
||
f"• آپلود فایلهای حجیم با نمایش درصد پیشرفت (Progress Bar)\n"
|
||
f"• قابلیت اکسترکت (Unzip) خودکار آرشیوها در پوشه اصلی پروژه\n"
|
||
f"• امکان درج یادداشت/پرامپت برای هوش مصنوعی به همراه فایل"
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("🌐 باز کردن صفحه آپلود", url=upload_url)],
|
||
[InlineKeyboardButton("📁 بازگشت به پروژهها", callback_data="proj_menu")],
|
||
]
|
||
else:
|
||
msg = (
|
||
f"📤 <b>Web File Upload for Project <code>{escape_html(p_name)}</code></b>\n\n"
|
||
f"Upload large files or ZIP archives (up to 2GB) directly to your project workspace:\n\n"
|
||
f"🔗 <b>Upload Link:</b>\n"
|
||
f"{upload_url}"
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("🌐 Open Upload Page", url=upload_url)],
|
||
[InlineKeyboardButton("📁 Back to Projects", callback_data="proj_menu")],
|
||
]
|
||
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
|
||
|
||
elif data.startswith("proj_backup:"):
|
||
target_name = data.split(":", 1)[1]
|
||
accessible = session_manager.get_all_accessible_projects(chat_id)
|
||
proj = accessible.get(target_name)
|
||
if not proj:
|
||
for k, p in accessible.items():
|
||
if p.name == target_name:
|
||
proj = p
|
||
break
|
||
if not proj:
|
||
await query.answer("❌ پروژه یافت نشد." if is_fa else "❌ Project not found.", show_alert=True)
|
||
return
|
||
|
||
await query.answer("⏳ در حال آمادهسازی بکاپ..." if is_fa else "⏳ Preparing backup...")
|
||
status_msg = await query.message.reply_html(
|
||
f"⏳ <i>در حال آمادهسازی و فشردهسازی بکاپ پروژه <code>{escape_html(proj.name)}</code>...</i>"
|
||
if is_fa else
|
||
f"⏳ <i>Preparing and compressing backup for project <code>{escape_html(proj.name)}</code>...</i>"
|
||
)
|
||
await handle_project_backup_flow(
|
||
application=context.application,
|
||
chat_id=chat_id,
|
||
proj_name=proj.name,
|
||
workspace=proj.workspace,
|
||
is_fa=is_fa,
|
||
status_msg=status_msg,
|
||
)
|
||
|
||
elif data == "proj_model_menu":
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
if not curr_proj:
|
||
await query.answer("پروژهای فعال نیست.", show_alert=True)
|
||
return
|
||
keyboard = []
|
||
for model_id, label in AVAILABLE_MODELS.items():
|
||
is_selected = "✅ " if model_id == curr_proj.model else ""
|
||
keyboard.append([InlineKeyboardButton(f"{is_selected}{label}", callback_data=f"set_model:{model_id}")])
|
||
keyboard.append([
|
||
InlineKeyboardButton("📁 پروژهها" if is_fa else "📁 Projects", callback_data="proj_menu"),
|
||
InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Dashboard", callback_data="btn_dashboard"),
|
||
])
|
||
await query.edit_message_text(
|
||
f"🧠 <b>Select AI Model</b>\nProject: <code>{escape_html(curr_proj.name)}</code>\nCurrent: <code>{curr_proj.model}</code>",
|
||
parse_mode=constants.ParseMode.HTML,
|
||
reply_markup=InlineKeyboardMarkup(keyboard),
|
||
)
|
||
|
||
elif data == "btn_memory_menu" or data.startswith("mem_tab:"):
|
||
parts = data.split(":")
|
||
v_type = parts[1] if len(parts) > 1 else "project"
|
||
page_num = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 0
|
||
if v_type == "global" and not is_admin_user:
|
||
await query.answer("🚫 مشاهده حافظه عمومی فقط برای مدیر سیستم مجاز است." if is_fa else "🚫 Global memory access restricted to administrators.", show_alert=True)
|
||
v_type = "project" if session_manager.get_current_project(chat_id) else "user"
|
||
page_num = 0
|
||
text, markup = build_memory_menu(chat_id, view_type=v_type, page=page_num)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("mem_del_conf:"):
|
||
_, m_id_str, v_type, page_str = data.split(":")
|
||
m_id = int(m_id_str)
|
||
p_num = int(page_str)
|
||
from memory_manager import memory_manager
|
||
mem = memory_manager.get_by_id(m_id)
|
||
if not mem:
|
||
await query.answer("❌ خاطره یافت نشد." if is_fa else "❌ Memory not found.", show_alert=True)
|
||
text, markup = build_memory_menu(chat_id, view_type=v_type, page=p_num)
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
elif mem.is_global and not is_admin_user:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
return
|
||
else:
|
||
confirm_text = (
|
||
f"🗑️ <b>آیا از حذف این خاطره اطمینان دارید؟</b>\n\n"
|
||
f"• 🔑 <b>کلید:</b> <code>{escape_html(mem.key)}</code>\n"
|
||
f"• 📝 <b>محتوا:</b> {escape_html(mem.content)}"
|
||
if is_fa
|
||
else
|
||
f"🗑️ <b>Are you sure you want to delete this memory?</b>\n\n"
|
||
f"• 🔑 <b>Key:</b> <code>{escape_html(mem.key)}</code>\n"
|
||
f"• 📝 <b>Content:</b> {escape_html(mem.content)}"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("✅ بله، حذف شود" if is_fa else "✅ Yes, Delete", callback_data=f"mem_del_do:{m_id}:{v_type}:{p_num}"),
|
||
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data=f"mem_tab:{v_type}:{p_num}"),
|
||
]
|
||
]
|
||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("mem_del_do:"):
|
||
_, m_id_str, v_type, page_str = data.split(":")
|
||
m_id = int(m_id_str)
|
||
p_num = int(page_str)
|
||
from memory_manager import memory_manager
|
||
mem = memory_manager.get_by_id(m_id)
|
||
if mem and mem.is_global and not is_admin_user:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
return
|
||
try:
|
||
ok = memory_manager.delete_by_id(m_id, user_id=chat_id, is_admin=is_admin_user)
|
||
if ok:
|
||
await query.answer("🗑️ خاطره با موفقیت حذف شد." if is_fa else "🗑️ Memory deleted successfully.")
|
||
else:
|
||
await query.answer("❌ خاطره یافت نشد." if is_fa else "❌ Memory not found.", show_alert=True)
|
||
except PermissionError:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
except Exception as e:
|
||
await query.answer(f"⚠️ خطا: {e}", show_alert=True)
|
||
|
||
text, markup = build_memory_menu(chat_id, view_type=v_type, page=p_num)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("mem_clear_conf:"):
|
||
v_type = data.split(":")[1]
|
||
if v_type == "global" and not is_admin_user:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
return
|
||
type_label = "عمومی سیستم" if v_type == "global" else "اختصاصی خودتان"
|
||
confirm_text = (
|
||
f"⚠️ <b>هشدار پاکسازی حافظه</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید تمام خاطرات <b>{type_label}</b> را به صورت کامل پاک کنید؟ این عملیات غیرقابل بازگشت است."
|
||
if is_fa
|
||
else
|
||
f"⚠️ <b>Warning: Clear Memory</b>\n\n"
|
||
f"Are you sure you want to permanently clear all <b>{v_type}</b> memories?"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("💥 بله، همه را پاک کن" if is_fa else "💥 Yes, Clear All", callback_data=f"mem_clear_do:{v_type}"),
|
||
InlineKeyboardButton("❌ انصراف" if is_fa else "❌ Cancel", callback_data=f"mem_tab:{v_type}:0"),
|
||
]
|
||
]
|
||
await query.edit_message_text(confirm_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("mem_clear_do:"):
|
||
v_type = data.split(":")[1]
|
||
if v_type == "global" and not is_admin_user:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
return
|
||
from memory_manager import memory_manager
|
||
curr_p = session_manager.get_current_project(chat_id)
|
||
p_name = curr_p.name if curr_p else "default"
|
||
try:
|
||
cnt = memory_manager.clear_memories(
|
||
type_=v_type,
|
||
user_id=chat_id,
|
||
project_name=p_name if v_type == "project" else None,
|
||
is_admin=is_admin_user,
|
||
)
|
||
await query.answer(f"🧹 تعداد {cnt} خاطره پاک شد." if is_fa else f"🧹 Cleared {cnt} memories.")
|
||
except PermissionError:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
except Exception as e:
|
||
await query.answer(f"⚠️ خطا: {e}", show_alert=True)
|
||
|
||
text, markup = build_memory_menu(chat_id, view_type=v_type, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("mem_add_menu:") or data.startswith("mem_add_tab:"):
|
||
parts = data.split(":")
|
||
v_type = parts[1] if len(parts) > 1 else "project"
|
||
if v_type == "global" and not is_admin_user:
|
||
await query.answer("🚫 افزودن حافظه عمومی فقط مختص مدیر سیستم است." if is_fa else "🚫 Global memory restricted to administrators.", show_alert=True)
|
||
v_type = "project" if session_manager.get_current_project(chat_id) else "user"
|
||
text, markup = build_memory_add_menu(chat_id, view_type=v_type)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("mem_add_preset:"):
|
||
_, target_type, preset_name = data.split(":", 2)
|
||
if target_type == "global" and not is_admin_user:
|
||
await query.answer("🚫 دسترسی غیرمجاز!" if is_fa else "🚫 Permission denied!", show_alert=True)
|
||
return
|
||
|
||
from memory_manager import memory_manager
|
||
curr_p = session_manager.get_current_project(chat_id)
|
||
p_name = curr_p.name if curr_p else "default"
|
||
|
||
preset_data = MEMORY_PRESETS.get(target_type, {}).get(preset_name)
|
||
if not preset_data:
|
||
await query.answer("❌ الگوی مورد نظر یافت نشد." if is_fa else "❌ Preset not found.", show_alert=True)
|
||
return
|
||
|
||
try:
|
||
item, is_created = memory_manager.save_or_update(
|
||
type_=target_type,
|
||
key=preset_data["key"],
|
||
content=preset_data["content"] if is_fa else preset_data.get("content_en", preset_data["content"]),
|
||
user_id=chat_id,
|
||
project_name=p_name if target_type == "project" else None,
|
||
category=preset_data.get("category", "general"),
|
||
importance=5 if target_type == "global" else 3,
|
||
created_by=chat_id,
|
||
)
|
||
action_str = "ثبت شد" if is_created else "بهروزرسانی شد"
|
||
await query.answer(f"✅ خاطره «{item.key}» با موفقیت {action_str}!" if is_fa else f"✅ Memory '{item.key}' saved!", show_alert=True)
|
||
except Exception as e:
|
||
await query.answer(f"⚠️ خطا در ثبت خاطره: {e}", show_alert=True)
|
||
|
||
text, markup = build_memory_menu(chat_id, view_type=target_type, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_tasks_menu":
|
||
text, markup = build_tasks_menu(chat_id, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("tasks_page:"):
|
||
page_num = int(data.split(":", 1)[1])
|
||
text, markup = build_tasks_menu(chat_id, page=page_num)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("task_detail:"):
|
||
task_id = data.split(":", 1)[1]
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("task_run:"):
|
||
task_id = data.split(":", 1)[1]
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
|
||
return
|
||
await query.answer("⏳ در حال اجرای تسک..." if is_fa else "⏳ Executing task...")
|
||
status_msg = await query.message.reply_html(
|
||
f"⏳ <b>در حال اجرای تسک <code>{task_id}</code>...</b>" if is_fa else f"⏳ <b>Executing task <code>{task_id}</code>...</b>"
|
||
)
|
||
success, out = await task_scheduler.execute_task_now(task_id, context.application)
|
||
try:
|
||
await status_msg.delete()
|
||
except Exception:
|
||
pass
|
||
|
||
elif data.startswith("task_pause:"):
|
||
task_id = data.split(":", 1)[1]
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
|
||
return
|
||
ok = task_scheduler.pause_task(task_id)
|
||
if ok:
|
||
await query.answer("⏸️ تسک متوقف شد." if is_fa else "⏸️ Task paused.")
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
else:
|
||
await query.answer("خطا در توقف تسک." if is_fa else "Failed to pause task.", show_alert=True)
|
||
|
||
elif data.startswith("task_resume:"):
|
||
task_id = data.split(":", 1)[1]
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
|
||
return
|
||
ok = task_scheduler.resume_task(task_id)
|
||
if ok:
|
||
await query.answer("▶️ تسک فعال شد." if is_fa else "▶️ Task resumed.")
|
||
text, markup = build_task_detail_menu(chat_id, task_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
else:
|
||
await query.answer("خطا در فعالسازی تسک." if is_fa else "Failed to resume task.", show_alert=True)
|
||
|
||
elif data.startswith("task_del:"):
|
||
task_id = data.split(":", 1)[1]
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
|
||
return
|
||
ok = task_scheduler.delete_task(task_id)
|
||
if ok:
|
||
await query.answer("🗑️ تسک با موفقیت حذف شد." if is_fa else "🗑️ Task deleted.")
|
||
text, markup = build_tasks_menu(chat_id, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
else:
|
||
await query.answer("❌ تسک یافت نشد." if is_fa else "❌ Task not found.", show_alert=True)
|
||
|
||
elif data.startswith("task_last_out:"):
|
||
task_id = data.split(":", 1)[1]
|
||
task = task_scheduler.get_task(task_id)
|
||
if not task or (task.chat_id != chat_id and task.creator_id != chat_id):
|
||
await query.answer("❌ تسک یافت نشد یا دسترسی مجاز نیست." if is_fa else "❌ Task not found or access denied.", show_alert=True)
|
||
return
|
||
if not task.last_run_result:
|
||
await query.answer("❌ خروجی ثبت نشده است." if is_fa else "❌ No output recorded.", show_alert=True)
|
||
return
|
||
await query.answer()
|
||
header = f"📜 <b>آخرین خروجی ثبت شده تسک <code>{task.id}</code>:</b>\n\n" if is_fa else f"📜 <b>Last Recorded Output for Task <code>{task.id}</code>:</b>\n\n"
|
||
if task.task_type == "prompt":
|
||
formatted = header + markdown_to_telegram_html(task.last_run_result)
|
||
else:
|
||
formatted = header + f"<pre>{escape_html(task.last_run_result)}</pre>"
|
||
chunks = split_message(formatted, max_length=settings.max_message_length)
|
||
for chunk in chunks:
|
||
await query.message.reply_html(chunk, disable_web_page_preview=True)
|
||
|
||
elif data == "task_clear_completed":
|
||
cnt = task_scheduler.clear_completed_tasks(chat_id)
|
||
await query.answer(f"🧹 {cnt} تسک پایانیافته پاکسازی شد." if is_fa else f"🧹 {cnt} completed tasks cleared.")
|
||
text, markup = build_tasks_menu(chat_id, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "task_new_guide":
|
||
text, markup = build_task_add_guide(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "req_access":
|
||
user = update.effective_user
|
||
if not user:
|
||
return
|
||
uid = user.id
|
||
if settings.is_user_authorized(uid):
|
||
await query.answer("✅ شما از قبل مجاز هستید." if is_fa else "✅ You are already authorized.", show_alert=True)
|
||
return
|
||
|
||
req = invite_manager.add_pending_request(
|
||
user_id=uid,
|
||
username=user.username or "",
|
||
first_name=user.first_name or "",
|
||
last_name=user.last_name or "",
|
||
)
|
||
await query.answer("✅ درخواست شما با موفقیت برای مدیران ارسال شد." if is_fa else "✅ Request sent to administrators.", show_alert=True)
|
||
|
||
if is_fa:
|
||
user_confirm = (
|
||
f"📩 <b>درخواست دسترسی شما ثبت شد!</b>\n\n"
|
||
f"اطلاعات شما (شناسه: <code>{uid}</code>) برای مدیران سیستم ارسال گردید.\n"
|
||
f"به محض تایید توسط مدیر، از طریق همین بات به شما اطلاع داده خواهد شد."
|
||
)
|
||
else:
|
||
user_confirm = (
|
||
f"📩 <b>Access Request Submitted!</b>\n\n"
|
||
f"Your request (User ID: <code>{uid}</code>) has been forwarded to administrators.\n"
|
||
f"You will be notified here as soon as your access is approved."
|
||
)
|
||
try:
|
||
await query.edit_message_text(user_confirm, parse_mode=constants.ParseMode.HTML)
|
||
except Exception:
|
||
pass
|
||
|
||
for admin_id in settings.admin_user_ids:
|
||
try:
|
||
admin_alert = (
|
||
f"🔔 <b>درخواست دسترسی کاربر جدید به ربات:</b>\n\n"
|
||
f"• 👤 <b>نام کاربر:</b> {escape_html(req.full_name)}\n"
|
||
f"• 🆔 <b>شناسه (User ID):</b> <code>{req.user_id}</code>\n"
|
||
f"• 🏷️ <b>نام کاربری:</b> @{escape_html(req.username) if req.username else '(ندارد)'}\n"
|
||
f"• 🕒 <b>زمان درخواست:</b> {format_timestamp(req.requested_at, is_fa=True)}\n"
|
||
)
|
||
adm_kb = [
|
||
[
|
||
InlineKeyboardButton("✅ تایید و صدور دسترسی", callback_data=f"req_approve:{req.user_id}"),
|
||
InlineKeyboardButton("❌ رد درخواست", callback_data=f"req_deny:{req.user_id}"),
|
||
]
|
||
]
|
||
await context.application.bot.send_message(
|
||
chat_id=admin_id,
|
||
text=admin_alert,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
reply_markup=InlineKeyboardMarkup(adm_kb),
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to send access request alert to admin {admin_id}: {e}")
|
||
|
||
elif data.startswith("req_approve:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
target_uid = int(data.split(":", 1)[1])
|
||
invite_manager.approve_request(target_uid)
|
||
await query.answer("✅ دسترسی کاربر تایید شد." if is_fa else "✅ User approved.")
|
||
try:
|
||
await query.edit_message_text(
|
||
f"✅ <b>دسترسی کاربر <code>{target_uid}</code> با موفقیت تایید و فعال شد.</b>",
|
||
parse_mode=constants.ParseMode.HTML,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
user_notif = (
|
||
f"🎉 <b>تبریک! دسترسی شما به ربات تایید شد.</b>\n\n"
|
||
f"مدیر سیستم دسترسی شما را فعال کرد.\n"
|
||
f"برای ورود به کنترلپنل و شروع گفتگو روی /start بزنید."
|
||
)
|
||
await context.application.bot.send_message(
|
||
chat_id=target_uid,
|
||
text=user_notif,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
elif data.startswith("req_deny:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
target_uid = int(data.split(":", 1)[1])
|
||
invite_manager.deny_request(target_uid)
|
||
await query.answer("❌ درخواست کاربر رد شد." if is_fa else "❌ Request denied.")
|
||
try:
|
||
await query.edit_message_text(
|
||
f"❌ <b>درخواست دسترسی کاربر <code>{target_uid}</code> رد شد.</b>",
|
||
parse_mode=constants.ParseMode.HTML,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
elif data == "btn_users_menu":
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
text, markup = build_users_menu(chat_id, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("users_page:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
page_num = int(data.split(":", 1)[1])
|
||
text, markup = build_users_menu(chat_id, page=page_num)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_invites_menu":
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
text, markup = build_invites_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_pending_reqs":
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
text, markup = build_pending_requests_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "btn_gen_invite":
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
bot_username = (await context.application.bot.get_me()).username or "AGYBot"
|
||
token = invite_manager.create_invite(creator_id=user_id, max_uses=1, duration_hours=None)
|
||
link = f"https://t.me/{bot_username}?start={token.code}"
|
||
if is_fa:
|
||
msg = (
|
||
f"🔗 <b>لینک دعوت یکبار مصرف جدید:</b>\n\n"
|
||
f"• 📋 <b>لینک:</b>\n<code>{link}</code>\n\n"
|
||
f"• 🔑 <b>کد:</b> <code>{token.code}</code>\n"
|
||
f"• 👥 <b>ظرفیت:</b> <code>۱ کاربر (یکبار مصرف)</code>\n"
|
||
f"• ⏳ <b>اعتبار:</b> <code>دائمی تا اولین استفاده</code>\n\n"
|
||
f"💡 <i>این لینک را برای کاربر مورد نظر ارسال کنید.</i>"
|
||
)
|
||
else:
|
||
msg = (
|
||
f"🔗 <b>Single-Use Invite Link Generated:</b>\n\n"
|
||
f"• 📋 <b>Link:</b>\n<code>{link}</code>\n\n"
|
||
f"• 🔑 <b>Code:</b> <code>{token.code}</code>\n"
|
||
f"• 👥 <b>Capacity:</b> <code>1 User (Single-Use)</code>\n\n"
|
||
f"💡 <i>Share this link with the invited user.</i>"
|
||
)
|
||
keyboard = [
|
||
[InlineKeyboardButton("📋 مشاهده لینکهای فعال" if is_fa else "📋 Active Links", callback_data="btn_invites_menu")],
|
||
[InlineKeyboardButton("👥 مدیریت کاربران" if is_fa else "👥 User Manager", callback_data="btn_users_menu")],
|
||
]
|
||
try:
|
||
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("user_revoke_ask:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
target_uid = int(data.split(":", 1)[1])
|
||
if is_fa:
|
||
msg = (
|
||
f"⚠️ <b>تأیید لغو دسترسی:</b>\n\n"
|
||
f"آیا مطمئن هستید که میخواهید دسترسی کاربر <code>{target_uid}</code> به ربات را لغو کنید؟"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🚫 بله، لغو دسترسی", callback_data=f"user_revoke_confirm:{target_uid}"),
|
||
InlineKeyboardButton("❌ انصراف", callback_data="btn_users_menu"),
|
||
]
|
||
]
|
||
else:
|
||
msg = (
|
||
f"⚠️ <b>Confirm Revoke Access:</b>\n\n"
|
||
f"Are you sure you want to revoke access for user <code>{target_uid}</code>?"
|
||
)
|
||
keyboard = [
|
||
[
|
||
InlineKeyboardButton("🚫 Yes, Revoke Access", callback_data=f"user_revoke_confirm:{target_uid}"),
|
||
InlineKeyboardButton("❌ Cancel", callback_data="btn_users_menu"),
|
||
]
|
||
]
|
||
try:
|
||
await query.edit_message_text(msg, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
except Exception:
|
||
await query.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
|
||
|
||
elif data.startswith("user_revoke_confirm:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
target_uid = int(data.split(":", 1)[1])
|
||
settings.remove_authorized_user(target_uid)
|
||
session_manager.cancel_active_task(target_uid)
|
||
await query.answer("🚫 دسترسی کاربر با موفقیت لغو شد." if is_fa else "🚫 User access revoked.")
|
||
text, markup = build_users_menu(chat_id, page=0)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data.startswith("invite_revoke:"):
|
||
if not settings.is_admin(user_id):
|
||
await query.answer("⛔ مخصوص مدیران" if is_fa else "⛔ Admin only", show_alert=True)
|
||
return
|
||
code_id = data.split(":", 1)[1]
|
||
invite_manager.revoke_invite(code_id)
|
||
await query.answer("🗑️ لینک دعوت باطل شد." if is_fa else "🗑️ Invite link revoked.")
|
||
text, markup = build_invites_menu(chat_id)
|
||
try:
|
||
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
|
||
except Exception:
|
||
await query.message.reply_html(text, reply_markup=markup)
|
||
|
||
elif data == "proj_close":
|
||
try:
|
||
await query.message.delete()
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
await query.answer()
|
||
except Exception:
|
||
pass
|
||
|
||
# Main Message & Prompt Processing Pipeline
|
||
@check_auth
|
||
async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
if not update.message or not update.message.text:
|
||
return
|
||
|
||
chat_id = update.effective_chat.id
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
session = session_manager.get_or_create(chat_id)
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
|
||
if not curr_proj:
|
||
if is_fa:
|
||
msg = (
|
||
"⚠️ <b>شما هنوز هیچ پروژهای ایجاد نکردهاید!</b>\n\n"
|
||
"برای ارسال دستورات و تعامل با هوش مصنوعی، لطفاً ابتدا یک پروژه ایجاد کنید:\n"
|
||
"<code>/newproject <نام_پروژه></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 <project_name></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 <نام_پروژه></code> یک پروژه بسازید."
|
||
if is_fa else
|
||
"⚠️ <b>You have not created any projects yet!</b>\n\n"
|
||
"Please create a project first using <code>/newproject <name></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 <نام_پروژه></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 <project_name></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)
|
||
|
||
model_to_use = getattr(curr_proj, "model", None) or getattr(session, "model", None) or settings.default_model or "gemini-3.7-flash-auto"
|
||
effort_to_use = getattr(curr_proj, "reasoning_effort", None) or getattr(session, "reasoning_effort", None)
|
||
model_display = get_model_display_name(model_to_use, effort_to_use)
|
||
|
||
# Initial thinking status message clearly stating which project and model are active
|
||
if status_msg_to_reuse:
|
||
status_msg = status_msg_to_reuse
|
||
else:
|
||
curr_cid = getattr(curr_proj, "conversation_id", None)
|
||
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
|
||
proj_label = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
|
||
status_lines = [f"🤔 <b>{escape_html(model_display)}</b>"]
|
||
if conv_title:
|
||
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
|
||
initial_status = f"{proj_label}\n\n" + "\n".join(status_lines)
|
||
|
||
status_msg = await update.effective_message.reply_html(
|
||
initial_status,
|
||
reply_markup=get_stop_button(session.language),
|
||
)
|
||
|
||
# Mark turn in progress for recovery upon crash/restart
|
||
session.turn_in_progress = True
|
||
session.last_prompt = prompt
|
||
session.last_status_msg_id = status_msg.message_id
|
||
session.last_delivered = False
|
||
session.last_response = ""
|
||
session.last_update_time = time.time()
|
||
session_manager.save()
|
||
|
||
accumulated_tokens = []
|
||
thoughts = []
|
||
tools = []
|
||
last_edit_time = 0.0
|
||
edit_lock = asyncio.Lock()
|
||
is_cancelled = False
|
||
|
||
async def update_telegram_display(final: bool = False):
|
||
nonlocal last_edit_time
|
||
if is_cancelled:
|
||
return
|
||
now = time.time()
|
||
if not final and (now - last_edit_time < settings.stream_edit_interval):
|
||
return
|
||
|
||
async with edit_lock:
|
||
if is_cancelled:
|
||
return
|
||
if not final and (time.time() - last_edit_time < settings.stream_edit_interval):
|
||
return
|
||
last_edit_time = time.time()
|
||
|
||
raw_text = "".join(accumulated_tokens)
|
||
session.last_response = raw_text
|
||
curr_proj.last_response = raw_text
|
||
|
||
# Get dynamic conversation topic if set or updated
|
||
curr_cid = getattr(curr_proj, "conversation_id", None)
|
||
conv_title = getattr(curr_proj, "conversation_titles", {}).get(curr_cid) if curr_cid and hasattr(curr_proj, "conversation_titles") else None
|
||
|
||
# Suffix at the BOTTOM of the message
|
||
bottom_parts = []
|
||
|
||
# 1. Thinking blockquote at the bottom with model name
|
||
if settings.enable_thinking_display and thoughts:
|
||
thought_content = "\n".join(thoughts[-3:])
|
||
bottom_parts.append(format_thought(thought_content, model_name=model_display, lang=session.language).strip())
|
||
|
||
# 2. Active tools at the bottom
|
||
if settings.enable_tool_notifications and tools:
|
||
bottom_parts.append("\n".join(tools[-2:]).strip())
|
||
|
||
# 3. Status indicator at the bottom stating which model is being used and active topic
|
||
if not final:
|
||
status_lines = [f"🤔 <b>{escape_html(model_display)}</b> ▌"]
|
||
if conv_title:
|
||
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
|
||
bottom_parts.append("\n".join(status_lines))
|
||
|
||
if raw_text.strip():
|
||
formatted_body = markdown_to_telegram_html(raw_text)
|
||
if bottom_parts:
|
||
full_html = formatted_body + "\n\n" + "\n\n".join(bottom_parts)
|
||
else:
|
||
full_html = formatted_body + (" ▌" if not final else "")
|
||
else:
|
||
# Still waiting for response text - show project header and thinking at the bottom
|
||
proj_header = f"📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>" if is_fa else f"📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>"
|
||
status_lines = [f"🤔 <b>{escape_html(model_display)}</b> ▌"]
|
||
if conv_title:
|
||
status_lines.append(f"🗣 <i>{escape_html(conv_title)}</i>")
|
||
thinking_msg = "\n".join(status_lines)
|
||
if bottom_parts:
|
||
full_html = proj_header + "\n\n" + "\n\n".join(bottom_parts)
|
||
else:
|
||
full_html = proj_header + "\n\n" + thinking_msg
|
||
|
||
if len(full_html) > settings.max_message_length:
|
||
suffix_str = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
|
||
available_for_body = max(200, settings.max_message_length - len(suffix_str) - 60)
|
||
trimmed_raw = raw_text[:available_for_body]
|
||
formatted_body = markdown_to_telegram_html(trimmed_raw)
|
||
more_indicator = ("...\n<i>(ادامه در پیام بعدی...)</i>" if is_fa else "...\n<i>(continues...)</i>")
|
||
full_html = formatted_body + more_indicator + suffix_str
|
||
if len(full_html) > settings.max_message_length:
|
||
full_html = full_html[:settings.max_message_length - 10] + "..."
|
||
|
||
try:
|
||
await status_msg.edit_text(
|
||
full_html,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
reply_markup=get_stop_button(session.language) if not final else None,
|
||
)
|
||
except BadRequest as e:
|
||
if "Message is not modified" not in str(e):
|
||
try:
|
||
fallback_body = escape_html(raw_text[:3500])
|
||
fallback_suffix = ("\n\n" + "\n\n".join(bottom_parts)) if bottom_parts else ""
|
||
fallback_msg = (fallback_body + fallback_suffix)[:settings.max_message_length]
|
||
await status_msg.edit_text(
|
||
fallback_msg,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
reply_markup=get_stop_button(session.language) if not final else None,
|
||
)
|
||
except Exception:
|
||
pass
|
||
except RetryAfter as e:
|
||
await asyncio.sleep(e.retry_after)
|
||
except Exception as e:
|
||
logger.debug(f"Streaming edit skipped: {e}")
|
||
|
||
def on_delta(token: str):
|
||
accumulated_tokens.append(token)
|
||
asyncio.create_task(update_telegram_display(final=False))
|
||
|
||
def on_thought(thought: str):
|
||
thoughts.append(thought)
|
||
asyncio.create_task(update_telegram_display(final=False))
|
||
|
||
def on_tool(name: str, args: str):
|
||
tools.append(format_tool_call(name, args, lang=session.language))
|
||
asyncio.create_task(update_telegram_display(final=False))
|
||
|
||
def on_model_change(new_model: str, new_effort: Optional[str] = None):
|
||
nonlocal model_display
|
||
model_display = get_model_display_name(new_model, new_effort)
|
||
asyncio.create_task(update_telegram_display(final=False))
|
||
|
||
def on_reset():
|
||
accumulated_tokens.clear()
|
||
|
||
async def send_typing_loop():
|
||
while True:
|
||
try:
|
||
await context.bot.send_chat_action(chat_id=chat_id, action=constants.ChatAction.TYPING)
|
||
await asyncio.sleep(4.5)
|
||
except asyncio.CancelledError:
|
||
break
|
||
except Exception:
|
||
await asyncio.sleep(4.5)
|
||
|
||
typing_task = asyncio.create_task(send_typing_loop())
|
||
|
||
async def run_agent_task():
|
||
try:
|
||
result = await AGYEngine.run_prompt(
|
||
session=session,
|
||
prompt=prompt,
|
||
on_delta=on_delta,
|
||
on_thought=on_thought,
|
||
on_tool=on_tool,
|
||
on_model_change=on_model_change,
|
||
on_reset=on_reset,
|
||
)
|
||
return result
|
||
finally:
|
||
typing_task.cancel()
|
||
|
||
task = asyncio.create_task(run_agent_task())
|
||
session_manager.active_tasks[chat_id] = task
|
||
|
||
try:
|
||
agent_result = await task
|
||
if isinstance(agent_result, AgentResult):
|
||
raw_text = agent_result.text
|
||
usage = agent_result.usage
|
||
duration = agent_result.duration
|
||
exec_model = agent_result.executed_model or curr_proj.model
|
||
exec_effort = agent_result.executed_effort or curr_proj.effort
|
||
else:
|
||
raw_text = agent_result if agent_result else "".join(accumulated_tokens)
|
||
usage = None
|
||
duration = 0.0
|
||
exec_model = curr_proj.model
|
||
exec_effort = curr_proj.effort
|
||
|
||
if not raw_text or not raw_text.strip() or raw_text.strip() == "<i>(Done with no text output)</i>":
|
||
if accumulated_tokens:
|
||
raw_text = "".join(accumulated_tokens)
|
||
else:
|
||
raw_text = "✅ **دستور و تغییرات درخواستی با موفقیت اعمال شدند.**" if is_fa else "✅ **Changes and requested operations were applied successfully.**"
|
||
|
||
# 1. Convert Markdown to Telegram HTML first
|
||
formatted_html = markdown_to_telegram_html(raw_text)
|
||
|
||
# 2. Intercept and execute all AI Bot Actions (Model switching, Effort, Languages, Projects, Tasks, Caddy, Buttons, Files, etc.)
|
||
formatted_html, created_tasks, executed_actions, inline_markup = await process_all_ai_actions(
|
||
raw_text=formatted_html,
|
||
chat_id=chat_id,
|
||
project_name=curr_proj.name,
|
||
is_fa=is_fa,
|
||
app=context.application,
|
||
)
|
||
|
||
chunks = split_message(formatted_html, max_length=settings.max_message_length)
|
||
|
||
if len(chunks) <= 1:
|
||
first_chunk = chunks[0] if chunks else "✅ <i>Done</i>"
|
||
try:
|
||
await status_msg.edit_text(first_chunk, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=inline_markup)
|
||
except Exception:
|
||
await status_msg.edit_text(escape_html(formatted_html[:settings.max_message_length]), parse_mode=constants.ParseMode.HTML, reply_markup=inline_markup)
|
||
else:
|
||
first_chunk = chunks[0] if chunks else "✅ <i>Done</i>"
|
||
try:
|
||
await status_msg.edit_text(first_chunk, parse_mode=constants.ParseMode.HTML, disable_web_page_preview=True, reply_markup=None)
|
||
except Exception:
|
||
await status_msg.edit_text(escape_html(formatted_html[:settings.max_message_length]), parse_mode=constants.ParseMode.HTML, reply_markup=None)
|
||
|
||
for i, follow_up in enumerate(chunks[1:]):
|
||
is_last = (i == len(chunks[1:]) - 1)
|
||
chunk_markup = inline_markup if is_last else None
|
||
try:
|
||
await update.effective_message.reply_html(follow_up, disable_web_page_preview=True, reply_markup=chunk_markup)
|
||
except Exception:
|
||
await update.effective_message.reply_text(follow_up, reply_markup=chunk_markup)
|
||
|
||
# Mark successfully completed and delivered
|
||
session.turn_in_progress = False
|
||
session.last_delivered = True
|
||
session.last_response = raw_text
|
||
curr_proj.last_response = raw_text
|
||
session.last_status_msg_id = None
|
||
session.last_update_time = time.time()
|
||
session_manager.save()
|
||
|
||
# Send context length & usage stats in a separate message after task completes
|
||
if settings.enable_context_length_message:
|
||
reset_action_names = {"RESET_CONVERSATION", "NEW_CONV", "NEW_CHAT", "CLEAR_CONTEXT", "RESET_CHAT"}
|
||
is_reset = any(act in reset_action_names for act in executed_actions)
|
||
if is_reset or curr_proj.last_context_length is None:
|
||
stats_usage = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
|
||
else:
|
||
stats_usage = usage
|
||
if not stats_usage and curr_proj.last_context_length:
|
||
stats_usage = {
|
||
"input_tokens": curr_proj.last_context_length,
|
||
"total_tokens": curr_proj.last_total_tokens or curr_proj.last_context_length,
|
||
}
|
||
|
||
if stats_usage:
|
||
stats_html = format_context_stats(
|
||
usage=stats_usage,
|
||
duration=duration,
|
||
model=exec_model,
|
||
effort=exec_effort,
|
||
lang=session.language,
|
||
project_name=curr_proj.name,
|
||
conversation_id=curr_proj.conversation_id,
|
||
)
|
||
try:
|
||
await update.effective_message.reply_html(
|
||
stats_html,
|
||
reply_markup=get_usage_buttons(session.language),
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception as stats_err:
|
||
logger.warning(f"Failed to send context stats message: {stats_err}")
|
||
|
||
except asyncio.CancelledError:
|
||
is_cancelled = True
|
||
session.turn_in_progress = False
|
||
session.last_delivered = True
|
||
session_manager.save()
|
||
try:
|
||
cancel_msg = "🛑 <b>عملیات و پردازش فعلی متوقف شد.</b>" if is_fa else "🛑 <b>Active task was stopped.</b>"
|
||
await status_msg.edit_text(cancel_msg, parse_mode=constants.ParseMode.HTML, reply_markup=None)
|
||
except Exception:
|
||
pass
|
||
except Exception as e:
|
||
session.turn_in_progress = False
|
||
session.last_delivered = True
|
||
session_manager.save()
|
||
logger.error(f"Error during agent turn: {e}", exc_info=True)
|
||
try:
|
||
await status_msg.edit_text(f"❌ <b>Error:</b> <code>{escape_html(str(e))}</code>", parse_mode=constants.ParseMode.HTML, reply_markup=None)
|
||
except Exception:
|
||
pass
|
||
finally:
|
||
session_manager.active_tasks.pop(chat_id, None)
|
||
typing_task.cancel()
|
||
if temp_file_to_cleanup:
|
||
try:
|
||
tf = Path(temp_file_to_cleanup)
|
||
if tf.exists():
|
||
tf.unlink()
|
||
except Exception as cleanup_err:
|
||
logger.warning(f"Failed to cleanup temp file {temp_file_to_cleanup}: {cleanup_err}")
|
||
|
||
async def on_startup(app: Application):
|
||
logger.info("Configuring Telegram bot command menu...")
|
||
try:
|
||
commands = [
|
||
BotCommand("start", "کنترلپنل اصلی / Main dashboard"),
|
||
BotCommand("memory", "حافظه هوش مصنوعی / AI Memory"),
|
||
BotCommand("projects", "مدیریت پروژهها / Manage projects"),
|
||
BotCommand("newproject", "ساخت پروژه جدید / Create project"),
|
||
BotCommand("switch", "سوییچ پروژه / Switch project"),
|
||
BotCommand("tasks", "زمانبندی تسکها / Scheduled tasks"),
|
||
BotCommand("schedule", "افزودن زمانبندی / Schedule task"),
|
||
BotCommand("conversations", "مدیریت گفتگوها / Conversations"),
|
||
BotCommand("switchconv", "سوییچ گفتگو / Switch chat"),
|
||
BotCommand("settopic", "تغییر موضوع گفتگو / Rename chat"),
|
||
BotCommand("delconv", "حذف گفتگو / Delete chat"),
|
||
BotCommand("clearconvs", "پاکسازی همه / Clear chats"),
|
||
BotCommand("lastconv", "آخرین گفتگو / Open last chat"),
|
||
BotCommand("share", "اشتراکگذاری پروژه / Share project"),
|
||
BotCommand("unshare", "لغو اشتراک پروژه / Unshare project"),
|
||
BotCommand("shared", "پروژههای اشتراکی / Shared projects"),
|
||
BotCommand("new", "گفتگوی جدید / New chat"),
|
||
BotCommand("compact", "فشردهسازی کانتکست / Compact context"),
|
||
BotCommand("usage", "سهمیه و مصرف / Quota & usage"),
|
||
BotCommand("model", "تغییر مدل / AI model"),
|
||
BotCommand("lang", "تنظیم زبان / Set language"),
|
||
BotCommand("effort", "سطح استدلال / Reasoning effort"),
|
||
BotCommand("workspace", "مسیر کاری / Workspace"),
|
||
BotCommand("status", "وضعیت ربات / System status"),
|
||
BotCommand("server", "سختافزار سرور / RAM, CPU, Disk"),
|
||
BotCommand("context", "آمار کانتکست / Context & tokens"),
|
||
BotCommand("cancel", "توقف عملیات / Stop task"),
|
||
BotCommand("last", "آخرین خروجی / Last AI output"),
|
||
BotCommand("invite", "دعوت کاربر / Invite user"),
|
||
BotCommand("users", "لیست کاربران / Users list"),
|
||
BotCommand("git", "مخزن گیت / Git repository"),
|
||
BotCommand("sync", "همگامسازی گیت / Git sync"),
|
||
BotCommand("commit", "کامیت و پوش / Git commit"),
|
||
BotCommand("backup", "بکاپ پروژه / Project backup"),
|
||
BotCommand("upload", "آپلود وب فایل حجیم / Web upload"),
|
||
BotCommand("help", "راهنمای دستورات / Bot guide"),
|
||
]
|
||
await app.bot.set_my_commands(commands)
|
||
except Exception as cmd_err:
|
||
logger.warning(f"Failed to set bot commands: {cmd_err}")
|
||
|
||
# Set Telegram App in web uploader module & start Web Upload server
|
||
try:
|
||
set_telegram_app(app)
|
||
asyncio.create_task(start_web_uploader_server())
|
||
except Exception as ue:
|
||
logger.error(f"Failed to start web uploader server: {ue}", exc_info=True)
|
||
|
||
# Start the background task scheduler
|
||
task_scheduler.start(app)
|
||
|
||
# Automatically ensure all projects have Gitea repos and are synced
|
||
try:
|
||
asyncio.create_task(git_manager.sync_all_projects())
|
||
except Exception as ge:
|
||
logger.warning(f"Failed to trigger startup git sync: {ge}")
|
||
|
||
logger.info("Checking active sessions after startup...")
|
||
await asyncio.sleep(1.0)
|
||
for chat_id, session in list(session_manager.sessions.items()):
|
||
curr_proj = session_manager.get_current_project(chat_id)
|
||
proj_name = curr_proj.name if curr_proj else "default"
|
||
is_admin = chat_id in settings.admin_user_ids
|
||
|
||
try:
|
||
# Check if there was an interrupted or un-delivered turn
|
||
if session.turn_in_progress or not session.last_delivered or session.last_status_msg_id:
|
||
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||
recovered_text = None
|
||
|
||
# Attempt to recover last output from AGY transcript
|
||
if curr_proj and curr_proj.conversation_id:
|
||
try:
|
||
from agy_engine import get_conversation_last_output
|
||
rec = get_conversation_last_output(curr_proj.conversation_id)
|
||
if rec.get("text"):
|
||
recovered_text = rec["text"]
|
||
elif rec.get("tools"):
|
||
tools_list = "\n".join(f"• <code>{escape_html(t)}</code>" for t in rec["tools"][-8:])
|
||
recovered_text = (
|
||
f"✅ <b>دستورات و ابزارهای زیر پیش از راهاندازی مجدد با موفقیت اعمال شدند:</b>\n\n{tools_list}"
|
||
if is_fa else
|
||
f"✅ <b>The following operations were executed before restart:</b>\n\n{tools_list}"
|
||
)
|
||
except Exception as rec_err:
|
||
logger.debug(f"Startup recovery failed for {chat_id}: {rec_err}")
|
||
|
||
if recovered_text:
|
||
formatted_html = markdown_to_telegram_html(recovered_text)
|
||
rec_header = (
|
||
f"🟢 <b>[پاسخ بازیابیشده پس از ریاستارت ربات - پروژه: <code>{escape_html(proj_name)}</code>]</b>\n\n"
|
||
if is_fa else
|
||
f"🟢 <b>[Response Recovered After Restart - Project: <code>{escape_html(proj_name)}</code>]</b>\n\n"
|
||
)
|
||
full_delivered = rec_header + formatted_html
|
||
chunks = split_message(full_delivered, max_length=settings.max_message_length)
|
||
|
||
if session.last_status_msg_id:
|
||
try:
|
||
await app.bot.edit_message_text(
|
||
chat_id=chat_id,
|
||
message_id=session.last_status_msg_id,
|
||
text=chunks[0],
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
)
|
||
for extra in chunks[1:]:
|
||
await app.bot.send_message(
|
||
chat_id=chat_id,
|
||
text=extra,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception:
|
||
for c in chunks:
|
||
try:
|
||
await app.bot.send_message(
|
||
chat_id=chat_id,
|
||
text=c,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception:
|
||
pass
|
||
else:
|
||
for c in chunks:
|
||
try:
|
||
await app.bot.send_message(
|
||
chat_id=chat_id,
|
||
text=c,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
session.last_response = recovered_text
|
||
if curr_proj:
|
||
curr_proj.last_response = recovered_text
|
||
|
||
elif is_admin and session.last_status_msg_id:
|
||
ready_msg = (
|
||
f"🟢 <b>ربات راهاندازی مجدد شد و آنلاین است (پروژه فعال: <code>{escape_html(proj_name)}</code>).</b>\n\nآماده دریافت دستورات جدید شما هستم!"
|
||
if is_fa else
|
||
f"🟢 <b>AGY Bot restarted successfully and is online (Active Project: <code>{escape_html(proj_name)}</code>)!</b>\n\nReady for your commands!"
|
||
)
|
||
try:
|
||
await app.bot.edit_message_text(
|
||
chat_id=chat_id,
|
||
message_id=session.last_status_msg_id,
|
||
text=ready_msg,
|
||
parse_mode=constants.ParseMode.HTML,
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception as edit_err:
|
||
logger.debug(f"Could not edit previous status msg {session.last_status_msg_id}: {edit_err}")
|
||
except Exception as e:
|
||
logger.error(f"Failed to deliver recovery message to chat {chat_id}: {e}", exc_info=True)
|
||
finally:
|
||
# Reset turn flags cleanly
|
||
session.turn_in_progress = False
|
||
session.restart_pending = False
|
||
session.last_delivered = True
|
||
session.last_status_msg_id = None
|
||
session_manager.save()
|
||
|
||
def main():
|
||
if not settings.telegram_bot_token:
|
||
logger.error("TELEGRAM_BOT_TOKEN is not set!")
|
||
sys.exit(1)
|
||
|
||
logger.info("Initializing Telegram Bot Application with concurrent_updates enabled...")
|
||
application = (
|
||
Application.builder()
|
||
.token(settings.telegram_bot_token)
|
||
.concurrent_updates(True)
|
||
.post_init(on_startup)
|
||
.build()
|
||
)
|
||
|
||
# Project Management Handlers
|
||
application.add_handler(CommandHandler(["projects", "project", "p"], projects_command))
|
||
application.add_handler(CommandHandler(["newproject", "createproject"], new_project_command))
|
||
application.add_handler(CommandHandler(["switch", "use", "switchproject"], switch_project_command))
|
||
application.add_handler(CommandHandler(["delproject", "deleteproject"], delete_project_command))
|
||
application.add_handler(CommandHandler("renameproject", rename_project_command))
|
||
application.add_handler(CommandHandler(["share", "shareproject"], share_command))
|
||
application.add_handler(CommandHandler(["unshare", "unshareproject"], unshare_command))
|
||
application.add_handler(CommandHandler(["shared", "sharedprojects"], shared_command))
|
||
application.add_handler(CommandHandler(["backup", "zip", "export"], backup_command))
|
||
application.add_handler(CommandHandler(["upload", "drop", "webupload"], upload_command))
|
||
|
||
# Git & Gitea Version Control Handlers
|
||
application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_command))
|
||
application.add_handler(CommandHandler(["sync", "gitsync", "pull"], sync_command))
|
||
application.add_handler(CommandHandler(["commit", "gitcommit"], commit_command))
|
||
application.add_handler(CommandHandler(["undo", "revert", "gitundo", "gitrevert", "laghv"], undo_command))
|
||
|
||
# Scheduled Tasks Handlers
|
||
application.add_handler(CommandHandler(["tasks", "task", "schedule", "schedules", "cron", "timer"], tasks_command))
|
||
|
||
# Admin User & Invite Handlers
|
||
application.add_handler(CommandHandler(["invite", "add_user", "adduser"], invite_command))
|
||
application.add_handler(CommandHandler(["uninvite", "revoke", "ban", "remove_user", "deluser"], uninvite_command))
|
||
application.add_handler(CommandHandler(["users", "whitelist", "members"], users_command))
|
||
application.add_handler(CommandHandler(["invitelink", "invite_link"], invitelink_command))
|
||
|
||
# General & Control Panel Handlers
|
||
application.add_handler(CommandHandler(["start", "menu", "dashboard", "panel"], start_command))
|
||
application.add_handler(CommandHandler(["memory", "memories", "hafeze"], memory_command))
|
||
application.add_handler(CommandHandler("help", help_command))
|
||
application.add_handler(CommandHandler(["usage", "quota", "credits"], usage_command))
|
||
application.add_handler(CommandHandler(["new", "reset"], new_command))
|
||
application.add_handler(CommandHandler(["compact", "compress", "fashorde"], compact_command))
|
||
application.add_handler(CommandHandler(["lastconv", "lastconversation", "openlast", "resume", "open_last"], last_conversation_command))
|
||
application.add_handler(CommandHandler(["conversations", "convs", "history_conv", "dialogs", "chats"], conversations_command))
|
||
application.add_handler(CommandHandler(["switchconv", "switch_conv", "openconv", "useconv", "selectconv"], switch_conv_command))
|
||
application.add_handler(CommandHandler(["settopic", "set_topic", "settitle", "set_title", "convtitle", "title"], set_conv_title_command))
|
||
application.add_handler(CommandHandler(["delconv", "deleteconv", "del_conv", "delete_conv", "removeconv"], delete_conv_command))
|
||
application.add_handler(CommandHandler(["clearconvs", "clear_convs", "deleteallconvs"], clear_convs_command))
|
||
application.add_handler(CommandHandler("model", model_command))
|
||
application.add_handler(CommandHandler(["lang", "language"], lang_command))
|
||
application.add_handler(CommandHandler("effort", effort_command))
|
||
application.add_handler(CommandHandler("workspace", workspace_command))
|
||
application.add_handler(CommandHandler("status", status_command))
|
||
application.add_handler(CommandHandler(["server", "hardware", "ram", "cpu", "sysinfo", "disk", "stats"], server_hardware_command))
|
||
application.add_handler(CommandHandler(["last", "output"], last_command))
|
||
application.add_handler(CommandHandler(["context", "tokens"], context_command))
|
||
application.add_handler(CommandHandler("cancel", cancel_command))
|
||
application.add_handler(CommandHandler(["restart", "reload"], restart_command))
|
||
application.add_handler(CommandHandler("auth", auth_command))
|
||
application.add_handler(CommandHandler("exec", exec_command))
|
||
|
||
# Callback Query Handler
|
||
application.add_handler(CallbackQueryHandler(callback_handler))
|
||
|
||
# Message Handlers
|
||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, message_handler))
|
||
application.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO | filters.VOICE | filters.AUDIO, file_handler))
|
||
|
||
# Global Error Handler
|
||
async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||
logger.error("Unhandled exception in telegram update handler:", exc_info=context.error)
|
||
|
||
application.add_error_handler(global_error_handler)
|
||
|
||
logger.info("Starting Telegram Bot Polling...")
|
||
application.run_polling(drop_pending_updates=False)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|