Add multi-user AGY authentication, OAuth login and profile isolation

This commit is contained in:
Antigravity Bot
2026-08-31 12:24:57 +03:30
parent 67bc7fb079
commit 4dc48ed3ab
4 changed files with 629 additions and 9 deletions
+3
View File
@@ -13,6 +13,7 @@ from typing import Optional, Dict, Any, Callable, List, Tuple
from dataclasses import dataclass, field, asdict
from config import settings
from auth_manager import auth_manager
logger = logging.getLogger(__name__)
@@ -1625,6 +1626,7 @@ class AGYEngine:
except Exception as pe:
logger.debug(f"Pre-prompt git pull skipped: {pe}")
user_env = auth_manager.get_user_env(session.chat_id)
start_time = time.time()
proc = await asyncio.create_subprocess_exec(
*cmd,
@@ -1632,6 +1634,7 @@ class AGYEngine:
stderr=asyncio.subprocess.PIPE,
limit=100 * 1024 * 1024, # 100 MB buffer limit to handle large stream-json chunks
cwd=workspace_path,
env=user_env,
start_new_session=True,
)
+309
View File
@@ -0,0 +1,309 @@
import os
import re
import json
import time
import shutil
import asyncio
import logging
from pathlib import Path
from typing import Optional, Dict, Any, Tuple
from datetime import datetime
logger = logging.getLogger("AGYAuthManager")
PROFILES_BASE_DIR = Path("/root/.gemini_profiles")
MASTER_TOKEN_PATH = Path("/root/.gemini/antigravity-cli/antigravity-oauth-token")
MASTER_SETTINGS_PATH = Path("/root/.gemini/antigravity-cli/settings.json")
def escape_pre(text: str) -> str:
"""Escapes HTML entities for pre block."""
if not text:
return ""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
class AuthManager:
"""
Manages multi-account authentication, isolated user profiles,
and OAuth login flows for Antigravity (AGY) CLI.
"""
def __init__(self, base_dir: Path = PROFILES_BASE_DIR):
self.base_dir = base_dir
self.base_dir.mkdir(parents=True, exist_ok=True)
# Stores active login subprocesses: {user_id: {"proc": Process, "url": str, "started_at": float}}
self.active_logins: Dict[int, Dict[str, Any]] = {}
def get_profile_dir(self, user_id: int) -> Path:
"""Returns the isolated home directory for a given user."""
return self.base_dir / str(user_id)
def get_config_dir(self, user_id: int) -> Path:
"""Returns the .gemini/antigravity-cli directory inside user profile."""
return self.get_profile_dir(user_id) / ".gemini" / "antigravity-cli"
def get_token_path(self, user_id: int) -> Path:
"""Returns the path to the user's antigravity-oauth-token."""
return self.get_config_dir(user_id) / "antigravity-oauth-token"
def has_custom_account(self, user_id: int) -> bool:
"""Checks if a user has a valid personal AGY account token configured."""
token_file = self.get_token_path(user_id)
if token_file.exists() and token_file.is_file() and token_file.stat().st_size > 10:
return True
return False
def get_user_home(self, user_id: int) -> str:
"""Returns the HOME directory path to use for this user."""
if self.has_custom_account(user_id):
return str(self.get_profile_dir(user_id))
return "/root"
def get_user_env(self, user_id: int) -> Dict[str, str]:
"""Returns environment variables dictionary with the proper HOME set for the user."""
env = os.environ.copy()
env["HOME"] = self.get_user_home(user_id)
return env
def get_account_status(self, user_id: int) -> Dict[str, Any]:
"""Returns detailed status of the user's active account."""
has_custom = self.has_custom_account(user_id)
token_path = self.get_token_path(user_id) if has_custom else MASTER_TOKEN_PATH
modified_time_str = "نامشخص"
if token_path.exists():
try:
mtime = token_path.stat().st_mtime
modified_time_str = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
pass
return {
"user_id": user_id,
"has_custom_account": has_custom,
"is_default_server": not has_custom,
"account_type": "حساب اختصاصی (Custom Account)" if has_custom else "حساب پیش‌فرض سرور (Server Default)",
"token_path": str(token_path),
"last_modified": modified_time_str,
"is_login_in_progress": user_id in self.active_logins,
}
async def start_oauth_login(self, user_id: int, timeout: int = 25) -> Tuple[bool, str, Optional[str]]:
"""
Starts headless OAuth login flow for a user:
Launches AGY CLI in a clean/temporary profile state, intercepts the OAuth URL,
and leaves the subprocess waiting for authorization code input.
"""
# Cancel any previous login session for this user
self.cancel_oauth_login(user_id)
config_dir = self.get_config_dir(user_id)
config_dir.mkdir(parents=True, exist_ok=True)
# Temporary backup existing token if user wants to re-login
token_file = self.get_token_path(user_id)
backup_token = None
if token_file.exists():
backup_token = token_file.read_text(encoding="utf-8", errors="ignore")
try:
token_file.unlink()
except Exception:
pass
env = os.environ.copy()
env["HOME"] = str(self.get_profile_dir(user_id))
try:
proc = await asyncio.create_subprocess_exec(
"agy", "--print", "/usage",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
cwd="/root",
start_new_session=True,
)
auth_url: Optional[str] = None
start_time = time.time()
url_pattern = re.compile(r"https://accounts\.google\.com/o/oauth2/auth\S+")
while time.time() - start_time < timeout:
try:
line_bytes = await asyncio.wait_for(proc.stdout.readline(), timeout=3.0)
if not line_bytes:
break
line_str = line_bytes.decode("utf-8", errors="replace").strip()
logger.debug(f"OAuth login stdout line for user {user_id}: {line_str}")
match = url_pattern.search(line_str)
if match:
auth_url = match.group(0).rstrip(").,")
break
if "Waiting for authentication" in line_str or "paste the authorization code" in line_str:
if auth_url:
break
except asyncio.TimeoutError:
if proc.returncode is not None:
break
continue
if not auth_url:
try:
proc.kill()
except Exception:
pass
if backup_token:
token_file.write_text(backup_token, encoding="utf-8")
token_file.chmod(0o600)
return False, "عدم دریافت لینک احراز هویت از سرور گوگل. لطفاً مجدداً تلاش فرمایید.", None
self.active_logins[user_id] = {
"proc": proc,
"url": auth_url,
"backup_token": backup_token,
"started_at": time.time(),
}
return True, "لینک احراز هویت گوگل با موفقیت ایجاد شد.", auth_url
except Exception as e:
logger.error(f"Error starting OAuth login for user {user_id}: {e}", exc_info=True)
if backup_token:
try:
token_file.write_text(backup_token, encoding="utf-8")
token_file.chmod(0o600)
except Exception:
pass
return False, f"خطا در ایجاد فرآیند لاگین: {e}", None
async def complete_oauth_login(self, user_id: int, auth_code: str, timeout: int = 40) -> Tuple[bool, str]:
"""
Completes the OAuth login flow by feeding the authorization code into the waiting AGY CLI process.
"""
if user_id not in self.active_logins:
return False, "هیچ فرآیند لاگین فعالی برای شما یافت نشد. لطفاً دستور /login را مجدداً ارسال کنید."
login_data = self.active_logins[user_id]
proc = login_data["proc"]
backup_token = login_data.get("backup_token")
token_file = self.get_token_path(user_id)
clean_code = auth_code.strip()
clean_code = re.sub(r"^[`'\"]+|[`'\"]+$", "", clean_code)
try:
input_bytes = (clean_code + "\n").encode("utf-8")
proc.stdin.write(input_bytes)
await proc.stdin.drain()
stdout_data, stderr_data = await asyncio.wait_for(proc.communicate(), timeout=timeout)
out_str = stdout_data.decode("utf-8", errors="replace")
err_str = stderr_data.decode("utf-8", errors="replace")
logger.info(f"OAuth complete output for {user_id}: out='{out_str[:150]}' err='{err_str[:150]}'")
if token_file.exists() and token_file.stat().st_size > 10:
token_file.chmod(0o600)
self.active_logins.pop(user_id, None)
return True, "🎉 تبریک! ورود با حساب گوگل اختصاصی شما با موفقیت انجام و ذخیره شد."
if backup_token:
token_file.write_text(backup_token, encoding="utf-8")
token_file.chmod(0o600)
self.active_logins.pop(user_id, None)
return False, f"کد تایید معتبر نبود یا فرآیند با خطا مواجه شد:\n<pre>{escape_pre(err_str or out_str)}</pre>"
except asyncio.TimeoutError:
try:
proc.kill()
except Exception:
pass
self.active_logins.pop(user_id, None)
if backup_token:
token_file.write_text(backup_token, encoding="utf-8")
token_file.chmod(0o600)
return False, "زمان تایید کد احراز هویت به پایان رسید (Timeout). لطفاً مجدداً با /login تلاش کنید."
except Exception as e:
logger.error(f"Error completing OAuth login for user {user_id}: {e}", exc_info=True)
self.active_logins.pop(user_id, None)
return False, f"خطا در ثبت کد احراز هویت: {e}"
def cancel_oauth_login(self, user_id: int) -> bool:
"""Cancels any pending login flow for a user."""
login_data = self.active_logins.pop(user_id, None)
if login_data:
proc = login_data.get("proc")
if proc:
try:
proc.kill()
except Exception:
pass
backup_token = login_data.get("backup_token")
token_file = self.get_token_path(user_id)
if backup_token and not token_file.exists():
try:
token_file.write_text(backup_token, encoding="utf-8")
token_file.chmod(0o600)
except Exception:
pass
return True
return False
def import_token(self, user_id: int, token_data_str: str) -> Tuple[bool, str]:
"""
Directly imports an existing antigravity-oauth-token content for the user.
"""
raw_text = token_data_str.strip()
try:
parsed = json.loads(raw_text)
if not isinstance(parsed, dict) or ("token" not in parsed and "access_token" not in parsed and "refresh_token" not in parsed):
return False, "فرمت فایل یا متن توکن معتبر نیست. توکن باید فایل JSON حاوی اطلاعات احراز هویت AGY باشد."
except Exception:
return False, "متن ارسالی یک ساختار JSON معتبر برای توکن AGY نیست."
config_dir = self.get_config_dir(user_id)
config_dir.mkdir(parents=True, exist_ok=True)
token_file = self.get_token_path(user_id)
try:
token_file.write_text(raw_text, encoding="utf-8")
token_file.chmod(0o600)
return True, "✅ فایل توکن حساب اختصاصی شما با موفقیت ثبت و فعال شد."
except Exception as e:
return False, f"خطا در ذخیره‌سازی توکن: {e}"
def logout_user(self, user_id: int) -> Tuple[bool, str]:
"""
Logs out a user by removing their custom token, falling back to server default.
"""
self.cancel_oauth_login(user_id)
token_file = self.get_token_path(user_id)
if token_file.exists():
try:
token_file.unlink()
return True, "حساب اختصاصی شما با موفقیت خارج شد. از این پس درخواست‌های شما با حساب پیش‌فرض سرور اجرا می‌شوند."
except Exception as e:
return False, f"خطا در حذف توکن اختصاصی: {e}"
return True, "شما از قبل در حال استفاده از حساب پیش‌فرض سرور هستید."
def copy_master_token_to_user(self, user_id: int) -> Tuple[bool, str]:
"""
Copies the current active server master token to a specific user's profile.
"""
if not MASTER_TOKEN_PATH.exists():
return False, "توکن فعال سرور یافت نشد."
config_dir = self.get_config_dir(user_id)
config_dir.mkdir(parents=True, exist_ok=True)
dst = self.get_token_path(user_id)
try:
shutil.copy2(MASTER_TOKEN_PATH, dst)
dst.chmod(0o600)
return True, "توکن فعال سرور با موفقیت در پروفایل شما ثبت گردید."
except Exception as e:
return False, f"خطا در کپی توکن سرور: {e}"
auth_manager = AuthManager()
+311 -8
View File
@@ -75,6 +75,7 @@ from remote_audit import remote_audit
from sys_monitor import render_server_hardware_report
from usage_monitor import fetch_and_render_usage_report
from bot_actions import process_all_ai_actions
from auth_manager import auth_manager
from web_uploader import create_upload_token, get_upload_url, start_web_uploader_server, set_telegram_app
LANG_ALIASES = {
@@ -217,11 +218,16 @@ def build_main_dashboard(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
ctx_info = f"• 📏 <b>طول کانتکست:</b> <code>{curr_proj.last_context_length:,} توکن</code>\n" if curr_proj.last_context_length else ""
ctx_info_en = f"• 📏 <b>Context Length:</b> <code>{curr_proj.last_context_length:,} tokens</code>\n" if curr_proj.last_context_length else ""
acc_status = auth_manager.get_account_status(chat_id)
acc_badge_fa = "🟢 اختصاصی" if acc_status["has_custom_account"] else "🟡 پیش‌فرض سرور"
acc_badge_en = "🟢 Custom" if acc_status["has_custom_account"] else "🟡 Server Default"
if is_fa:
text = (
f"🎛 <b>کنترل‌پنل مدیریت Antigravity (AGY)</b>\n\n"
f"• 📁 <b>پروژه فعال:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str}\n"
f"• 📂 <b>مسیر کاری:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🔑 <b>حساب AGY:</b> <b>{acc_badge_fa}</b>\n"
f"• 🧠 <b>مدل هوش مصنوعی:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>سطح استدلال:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>زبان پاسخ‌دهی:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
@@ -238,7 +244,8 @@ def build_main_dashboard(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
],
[
InlineKeyboardButton("🧠 حافظه هوش مصنوعی", callback_data="btn_memory_menu"),
InlineKeyboardButton("📈 سهمیه مصرف (AGY)", callback_data="btn_usage_menu"),
InlineKeyboardButton("📈 سهمیه مصرف", callback_data="btn_usage_menu"),
InlineKeyboardButton("🔑 حساب AGY", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 سخت‌افزار سرور", callback_data="btn_hw_menu"),
@@ -261,6 +268,7 @@ def build_main_dashboard(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
f"🎛 <b>Antigravity (AGY) Control Panel</b>\n\n"
f"• 📁 <b>Active Project:</b> <code>{escape_html(curr_proj.name)}</code> {owner_str_en}\n"
f"• 📂 <b>Workspace:</b> <code>{escape_html(curr_proj.workspace)}</code>\n"
f"• 🔑 <b>AGY Account:</b> <b>{acc_badge_en}</b>\n"
f"• 🧠 <b>AI Model:</b> <code>{curr_proj.model}</code>\n"
f"• ⚡ <b>Reasoning Effort:</b> <code>{curr_proj.effort}</code>\n"
f"• 🌐 <b>Language:</b> <code>{get_lang_display(curr_proj.language)}</code>\n"
@@ -278,6 +286,7 @@ def build_main_dashboard(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
[
InlineKeyboardButton("🧠 AI Memory", callback_data="btn_memory_menu"),
InlineKeyboardButton("📈 Quota & Usage", callback_data="btn_usage_menu"),
InlineKeyboardButton("🔑 AGY Account", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 Server Hardware", callback_data="btn_hw_menu"),
@@ -975,6 +984,9 @@ def build_settings_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
InlineKeyboardButton("🌐 زبان پاسخ‌دهی", callback_data="btn_lang_menu"),
InlineKeyboardButton("📊 وضعیت و کانتکست", callback_data="btn_status_menu"),
],
[
InlineKeyboardButton("🔑 حساب کاربری AGY (لاگین گوگل)", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("📦 دریافت فایل بکاپ (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 اشتراک‌گذاری پروژه", callback_data="proj_share_menu"),
@@ -1007,6 +1019,9 @@ def build_settings_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
InlineKeyboardButton("🌐 Response Language", callback_data="btn_lang_menu"),
InlineKeyboardButton("📊 Status & Tokens", callback_data="btn_status_menu"),
],
[
InlineKeyboardButton("🔑 AGY Account & Login", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("📦 Project Backup (Zip)", callback_data=f"proj_backup:{curr_proj.name}"),
InlineKeyboardButton("🤝 Share Project", callback_data="proj_share_menu"),
@@ -1018,24 +1033,124 @@ def build_settings_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
return text, InlineKeyboardMarkup(keyboard)
def build_account_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive AGY Account Management submenu."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
is_admin_user = settings.is_admin(chat_id)
status = auth_manager.get_account_status(chat_id)
has_custom = status["has_custom_account"]
login_in_progress = status["is_login_in_progress"]
if is_fa:
if has_custom:
acc_type_badge = "🟢 <b>حساب اختصاصی فعال (Custom Account)</b>"
desc = (
"درخواست‌ها و مصرف مدل‌های هوش مصنوعی شما مستقیماً از <b>حساب گوگل اختصاصی خودتان</b> کسر می‌شود.\n"
f"• 📅 <i>تاریخ ثبت/بروزرسانی توکن:</i> <code>{status['last_modified']}</code>\n"
)
else:
acc_type_badge = "🟡 <b>حساب پیش‌فرض سرور (Server Shared)</b>"
desc = (
"شما هم‌اکنون از حساب عمومی سرور استفاده می‌کنید.\n"
"برای استفاده از سهمیه‌های اختصاصی، اعتبارات شخصی و حفظ کامل حریم کوتای خود، می‌توانید با حساب گوگل خود لاگین کنید.\n"
)
text = (
f"🔑 <b>مدیریت حساب کاربری هوش مصنوعی (AGY Account)</b>\n\n"
f"• 🆔 <b>شناسه کاربر:</b> <code>{chat_id}</code>\n"
f"• 👤 <b>وضعیت حساب:</b> {acc_type_badge}\n\n"
f"{desc}\n"
f"💡 <i>روش‌های ورود و اتصال:</i>\n"
f"۱. <b>ورود اینتراکتیو با گوگل:</b> دکمه «🔑 ورود به حساب گوگل» را بزنید.\n"
f"۲. <b>ارسال مستقیم فایل یا متن توکن:</b> می‌توانید فایل <code>antigravity-oauth-token</code> یا محتوای JSON توکن خود را در چت ارسال کنید."
)
buttons = []
if login_in_progress:
buttons.append([
InlineKeyboardButton("❌ لغو فرآیند لاگین جاری", callback_data="btn_account_cancel_login"),
])
else:
buttons.append([
InlineKeyboardButton("🔑 ورود با حساب گوگل (OAuth)", callback_data="btn_account_start_login"),
])
if has_custom:
buttons.append([
InlineKeyboardButton("🚪 خروج از حساب شخصی", callback_data="btn_account_logout"),
])
elif is_admin_user:
buttons.append([
InlineKeyboardButton("📋 کپی توکن فعال سرور برای من", callback_data="btn_account_copy_server"),
])
buttons.append([
InlineKeyboardButton("📊 استعلام سهمیه حساب", callback_data="btn_usage_menu"),
InlineKeyboardButton("⚙️ تنظیمات", callback_data="btn_settings_menu"),
])
buttons.append([
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
])
else:
if has_custom:
acc_type_badge = "🟢 <b>Active Custom Account</b>"
desc = f"All AI interactions are using your personal Google account quota.\n• 📅 <i>Last modified:</i> <code>{status['last_modified']}</code>\n"
else:
acc_type_badge = "🟡 <b>Server Default Account (Shared)</b>"
desc = "You are using the shared server account.\nLog in with your own Google account for isolated personal quotas.\n"
text = (
f"🔑 <b>AGY Account Management</b>\n\n"
f"• 🆔 <b>User ID:</b> <code>{chat_id}</code>\n"
f"• 👤 <b>Account Status:</b> {acc_type_badge}\n\n"
f"{desc}\n"
f"💡 <i>Login options:</i>\n"
f"1. <b>Google OAuth:</b> Tap 'Log in with Google' button below.\n"
f"2. <b>Direct Token:</b> Upload your <code>antigravity-oauth-token</code> file or JSON text."
)
buttons = []
if login_in_progress:
buttons.append([InlineKeyboardButton("❌ Cancel Pending Login", callback_data="btn_account_cancel_login")])
else:
buttons.append([InlineKeyboardButton("🔑 Log in with Google", callback_data="btn_account_start_login")])
if has_custom:
buttons.append([InlineKeyboardButton("🚪 Log out from Custom Account", callback_data="btn_account_logout")])
elif is_admin_user:
buttons.append([InlineKeyboardButton("📋 Copy Server Master Token to Me", callback_data="btn_account_copy_server")])
buttons.append([
InlineKeyboardButton("📊 Check Quota", callback_data="btn_usage_menu"),
InlineKeyboardButton("⚙️ Settings", callback_data="btn_settings_menu"),
])
buttons.append([
InlineKeyboardButton("🏠 Dashboard", callback_data="btn_dashboard"),
])
return text, InlineKeyboardMarkup(buttons)
async def build_usage_report(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Queries AGY CLI /usage and /credits and generates an interactive report with visual progress bars."""
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
text = await fetch_and_render_usage_report(is_fa=is_fa)
text = await fetch_and_render_usage_report(is_fa=is_fa, user_id=chat_id)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🔄 بروزرسانی مصرف", callback_data="btn_usage_refresh"),
InlineKeyboardButton("🔑 مدیریت حساب AGY", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 سخت‌افزار سرور", callback_data="btn_hw_menu"),
InlineKeyboardButton("📁 پروژه‌ها", callback_data="proj_menu"),
],
[
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🔙 بستن", callback_data="proj_close"),
],
]
@@ -1043,13 +1158,14 @@ async def build_usage_report(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
keyboard = [
[
InlineKeyboardButton("🔄 Refresh Quota", callback_data="btn_usage_refresh"),
InlineKeyboardButton("🔑 AGY Account", callback_data="btn_account_menu"),
],
[
InlineKeyboardButton("🖥 Server Hardware", callback_data="btn_hw_menu"),
],
[
InlineKeyboardButton("📁 Projects", callback_data="proj_menu"),
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
InlineKeyboardButton("🔙 Close", callback_data="proj_close"),
],
]
@@ -4038,6 +4154,74 @@ async def usage_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
text, markup = await build_usage_report(chat_id)
await status_msg.edit_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
# Command: /account or /profile_auth or /acc
@check_auth
async def account_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = build_account_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup)
# Command: /login or /signin
@check_auth
async def login_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if context.args:
# User provided auth code directly: /login 4/0A...
auth_code = " ".join(context.args).strip()
wait_msg = await update.message.reply_html("⏳ <i>در حال ثبت و بررسی کد احراز هویت گوگل...</i>" if is_fa else "⏳ <i>Submitting and verifying Google Auth code...</i>")
success, msg = await auth_manager.complete_oauth_login(chat_id, auth_code)
if success:
text, markup = build_account_menu(chat_id)
await wait_msg.edit_text(f"{msg}\n\n{text}", parse_mode=constants.ParseMode.HTML, reply_markup=markup)
else:
await wait_msg.edit_text(msg, parse_mode=constants.ParseMode.HTML)
return
wait_msg = await update.message.reply_html("⏳ <i>در حال ایجاد نشست و لینک ورود اختصاصی گوگل...</i>" if is_fa else "⏳ <i>Generating Google OAuth login URL...</i>")
success, msg, auth_url = await auth_manager.start_oauth_login(chat_id)
if not success or not auth_url:
await wait_msg.edit_text(f"{msg}", parse_mode=constants.ParseMode.HTML)
return
if is_fa:
prompt_text = (
"🔑 <b>ورود به حساب اختصاصی گوگل (AGY OAuth Login)</b>\n\n"
"۱. روی دکمه زیر کلیک کنید تا صفحه رسمی ورود به حساب گوگل باز شود.\n"
"۲. وارد حساب کاربری گوگل خود شوید و اجازه دسترسی را تایید نمایید.\n"
"۳. <b>کد تایید احراز هویت (Authorization Code)</b> نمایش داده شده را کپی کرده و در پاسخ به همین پیام برای ربات ارسال کنید (یا دستور <code>/login &lt;کد&gt;</code> را بفرستید).\n\n"
"⏳ <i>فرصت ارسال کد: ۶۰ ثانیه</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 ورود به حساب کاربری گوگل", url=auth_url)],
[InlineKeyboardButton("❌ انصراف / لغو", callback_data="btn_account_cancel_login")],
]
else:
prompt_text = (
"🔑 <b>Google Account Login (AGY OAuth)</b>\n\n"
"1. Click the button below to open Google's authorization page.\n"
"2. Sign in to your Google Account and grant access permissions.\n"
"3. Copy the <b>Authorization Code</b> and paste it as a message to this bot.\n\n"
"⏳ <i>Timeout: 60 seconds</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 Sign in with Google", url=auth_url)],
[InlineKeyboardButton("❌ Cancel", callback_data="btn_account_cancel_login")],
]
await wait_msg.edit_text(prompt_text, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
# Command: /logout or /signout
@check_auth
async def logout_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
success, msg = auth_manager.logout_user(chat_id)
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{'' if success else ''} {msg}\n\n{text}", reply_markup=markup)
# Command: /memory or /memories
@check_auth
async def memory_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -5732,6 +5916,76 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_menu":
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_start_login":
await query.answer("⏳ در حال ایجاد نشست احراز هویت گوگل..." if is_fa else "⏳ Starting Google OAuth session...")
success, msg, auth_url = await auth_manager.start_oauth_login(chat_id)
if not success or not auth_url:
await query.message.reply_html(f"{msg}")
return
if is_fa:
prompt_text = (
"🔑 <b>ورود به حساب اختصاصی گوگل (AGY OAuth Login)</b>\n\n"
"۱. روی دکمه زیر کلیک کنید تا صفحه رسمی گوگل باز شود.\n"
"۲. وارد حساب کاربری گوگل خود شوید و اجازه دسترسی را تایید نمایید.\n"
"۳. <b>کد تایید احراز هویت (Authorization Code)</b> نمایش داده شده را کپی کرده و در چت ارسال کنید.\n\n"
"⏳ <i>فرصت ارسال کد: ۶۰ ثانیه</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 ورود به حساب کاربری گوگل", url=auth_url)],
[InlineKeyboardButton("❌ انصراف / لغو", callback_data="btn_account_cancel_login")],
]
else:
prompt_text = (
"🔑 <b>Google Account Login (AGY OAuth)</b>\n\n"
"1. Click the button below to open Google's authorization page.\n"
"2. Sign in to your Google Account and grant access permissions.\n"
"3. Copy the <b>Authorization Code</b> and paste it in chat.\n\n"
"⏳ <i>Timeout: 60 seconds</i>"
)
keyboard = [
[InlineKeyboardButton("🌐 Sign in with Google", url=auth_url)],
[InlineKeyboardButton("❌ Cancel", callback_data="btn_account_cancel_login")],
]
await query.message.reply_html(prompt_text, reply_markup=InlineKeyboardMarkup(keyboard), disable_web_page_preview=True)
elif data == "btn_account_cancel_login":
auth_manager.cancel_oauth_login(chat_id)
await query.answer("فرآیند لاگین لغو شد." if is_fa else "Login canceled.")
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_logout":
success, msg = auth_manager.logout_user(chat_id)
await query.answer(msg, show_alert=True)
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_account_copy_server":
if not is_admin_user:
await query.answer("⛔ فقط مدیر مجاز به کپی توکن سرور است.", show_alert=True)
return
success, msg = auth_manager.copy_master_token_to_user(chat_id)
await query.answer(msg, show_alert=True)
text, markup = build_account_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup)
except Exception:
await query.message.reply_html(text, reply_markup=markup)
elif data == "btn_status_menu":
accessible = session_manager.get_all_accessible_projects(chat_id)
if not curr_proj:
@@ -7095,6 +7349,33 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
# 0. Check if user has an active OAuth login waiting for authorization code
if chat_id in auth_manager.active_logins:
code_input = update.message.text.strip()
if code_input.lower() in ("/cancel", "لغو", "انصراف", "cancel"):
auth_manager.cancel_oauth_login(chat_id)
text, markup = build_account_menu(chat_id)
await update.message.reply_html("❌ فرآیند لاگین گوگل لغو گردید.\n\n" + text, reply_markup=markup)
return
wait_msg = await update.message.reply_html("⏳ <i>در حال ثبت و فعال‌سازی حساب کاربری با کد دریافتی...</i>" if is_fa else "⏳ <i>Completing login with authorization code...</i>")
success, msg = await auth_manager.complete_oauth_login(chat_id, code_input)
if success:
text, markup = build_account_menu(chat_id)
await wait_msg.edit_text(f"{msg}\n\n{text}", parse_mode=constants.ParseMode.HTML, reply_markup=markup)
else:
await wait_msg.edit_text(msg, parse_mode=constants.ParseMode.HTML)
return
# Check if user sent a JSON token directly in text
raw_stripped = update.message.text.strip()
if raw_stripped.startswith("{") and raw_stripped.endswith("}") and ("token" in raw_stripped or "access_token" in raw_stripped):
success, msg = auth_manager.import_token(chat_id, raw_stripped)
if success:
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
# 1. Check if user is in an interactive input state (e.g. configuring FTP fields)
if chat_id in USER_INPUT_STATES:
state = USER_INPUT_STATES.pop(chat_id)
@@ -7287,6 +7568,22 @@ async def file_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
session = session_manager.get_or_create(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
# Check if uploaded document is antigravity-oauth-token or a token json
if message.document:
doc_name = (message.document.file_name or "").lower()
if "oauth-token" in doc_name or doc_name.endswith(".token") or (doc_name.endswith(".json") and "token" in doc_name):
try:
doc_file = await message.document.get_file()
content_bytes = await doc_file.download_as_bytearray()
content_str = content_bytes.decode("utf-8", errors="ignore")
success, msg = auth_manager.import_token(chat_id, content_str)
if success:
text, markup = build_account_menu(chat_id)
await update.message.reply_html(f"{msg}\n\n{text}", reply_markup=markup)
return
except Exception as ex:
logger.error(f"Failed to process uploaded token file: {ex}")
if not curr_proj:
msg = (
"⚠️ <b>شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید!</b>\n\n"
@@ -7816,6 +8113,9 @@ async def on_startup(app: Application):
BotCommand("new", "گفتگوی جدید / New chat"),
BotCommand("compact", "فشرده‌سازی کانتکست / Compact context"),
BotCommand("usage", "سهمیه و مصرف / Quota & usage"),
BotCommand("account", "حساب کاربری AGY / AGY Account"),
BotCommand("login", "ورود به حساب گوگل / Google Login"),
BotCommand("logout", "خروج از حساب / Logout"),
BotCommand("model", "تغییر مدل / AI model"),
BotCommand("lang", "تنظیم زبان / Set language"),
BotCommand("effort", "سطح استدلال / Reasoning effort"),
@@ -8012,6 +8312,9 @@ def main():
application.add_handler(CommandHandler(["memory", "memories", "hafeze"], memory_command))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler(["usage", "quota", "credits"], usage_command))
application.add_handler(CommandHandler(["account", "profile_auth", "acc"], account_command))
application.add_handler(CommandHandler(["login", "google_login", "signin"], login_command))
application.add_handler(CommandHandler(["logout", "signout"], logout_command))
application.add_handler(CommandHandler(["new", "reset"], new_command))
application.add_handler(CommandHandler(["compact", "compress", "fashorde"], compact_command))
application.add_handler(CommandHandler(["lastconv", "lastconversation", "openlast", "resume", "open_last"], last_conversation_command))
+6 -1
View File
@@ -1,3 +1,4 @@
import os
import re
import time
import asyncio
@@ -122,8 +123,10 @@ def parse_agy_usage_output(usage_raw: str, credits_raw: str) -> Dict[str, Any]:
return data
async def fetch_and_render_usage_report(is_fa: bool = True) -> str:
async def fetch_and_render_usage_report(is_fa: bool = True, user_id: Optional[int] = None) -> str:
"""Executes agy /usage and /credits and formats with visual progress bars."""
from auth_manager import auth_manager
env = auth_manager.get_user_env(user_id) if user_id else os.environ.copy()
usage_raw = ""
credits_raw = ""
try:
@@ -131,6 +134,7 @@ async def fetch_and_render_usage_report(is_fa: bool = True) -> str:
"agy", "--print", "/usage",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
)
out1, _ = await proc1.communicate()
usage_raw = out1.decode("utf-8", errors="replace").strip()
@@ -139,6 +143,7 @@ async def fetch_and_render_usage_report(is_fa: bool = True) -> str:
"agy", "--print", "/credits",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
)
out2, _ = await proc2.communicate()
credits_raw = out2.decode("utf-8", errors="replace").strip()