import os
import re
import time
import asyncio
import logging
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional, Tuple
logger = logging.getLogger("AGYUsageMonitor")
def make_quota_bar(percent_remaining: float, length: int = 10) -> str:
"""
Generates an emoji progress bar for remaining quota:
>= 60%: 🟩 Green (Safe/High)
20% - 59%: 🟨 Yellow (Medium)
< 20%: 🟥 Red (Low)
"""
percent = max(0.0, min(100.0, float(percent_remaining)))
filled = int(round((percent / 100.0) * length))
filled = max(0, min(length, filled))
if percent >= 60:
fill_char = "🟩"
elif percent >= 20:
fill_char = "🟨"
else:
fill_char = "🟥"
empty_char = "⬜"
return f"{fill_char * filled}{empty_char * (length - filled)}"
def parse_iso_time_remaining(iso_str: str, is_fa: bool = True) -> str:
"""Calculates relative time remaining until reset timestamp."""
if not iso_str:
return "نامشخص" if is_fa else "Unknown"
try:
clean_iso = iso_str.strip().replace("Z", "+00:00")
target_dt = datetime.fromisoformat(clean_iso)
now_dt = datetime.now(timezone.utc)
diff_sec = int((target_dt - now_dt).total_seconds())
if diff_sec <= 0:
return "هماکنون در حال تمدید" if is_fa else "Resetting now"
days = diff_sec // 86400
hours = (diff_sec % 86400) // 3600
mins = (diff_sec % 3600) // 60
if is_fa:
parts = []
if days > 0:
parts.append(f"{days} روز")
if hours > 0:
parts.append(f"{hours} ساعت")
if mins > 0 or not parts:
parts.append(f"{mins} دقیقه")
return f"{' و '.join(parts)} دیگر"
else:
parts = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
parts.append(f"{mins}m")
return f"in {' '.join(parts)}"
except Exception as e:
return iso_str
def parse_agy_usage_output(usage_raw: str, credits_raw: str) -> Dict[str, Any]:
"""Parses raw stdout from 'agy --print /usage' and 'agy --print /credits'."""
data: Dict[str, Any] = {
"gemini_5h": None,
"gemini_5h_reset": "",
"gemini_weekly": None,
"gemini_weekly_reset": "",
"claude_5h": None,
"claude_5h_reset": "",
"claude_weekly": None,
"claude_weekly_reset": "",
"credits": "0",
"raw_text": usage_raw + "\n" + credits_raw,
}
lines = (usage_raw + "\n" + credits_raw).splitlines()
for line in lines:
line_clean = line.strip()
if not line_clean:
continue
# Check Gemini 5H
if re.search(r"Gemini.*Five Hour Limit Remaining", line_clean, re.I):
m = re.search(r"(\d+)%\s+(\S+)", line_clean)
if m:
data["gemini_5h"] = int(m.group(1))
data["gemini_5h_reset"] = m.group(2)
# Check Gemini Weekly
elif re.search(r"Gemini.*Weekly Limit Remaining", line_clean, re.I):
m = re.search(r"(\d+)%\s+(\S+)", line_clean)
if m:
data["gemini_weekly"] = int(m.group(1))
data["gemini_weekly_reset"] = m.group(2)
# Check Claude/GPT 5H
elif re.search(r"(Claude|GPT).*Five Hour Limit Remaining", line_clean, re.I):
m = re.search(r"(\d+)%\s+(\S+)", line_clean)
if m:
data["claude_5h"] = int(m.group(1))
data["claude_5h_reset"] = m.group(2)
# Check Claude/GPT Weekly
elif re.search(r"(Claude|GPT).*Weekly Limit Remaining", line_clean, re.I):
m = re.search(r"(\d+)%\s+(\S+)", line_clean)
if m:
data["claude_weekly"] = int(m.group(1))
data["claude_weekly_reset"] = m.group(2)
# Check credits
elif re.search(r"Remaining credits", line_clean, re.I):
m = re.search(r"Remaining credits\s+(\d+)", line_clean, re.I)
if m:
data["credits"] = m.group(1)
return data
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
if user_id and not auth_manager.has_custom_account(user_id):
if is_fa:
return (
"⚠️ حساب کاربری هوش مصنوعی متصل نیست!\n\n"
"در این ربات حسابهای اشتراکی غیرفعال هستند و هر کاربر باید حساب گوگل اختصاصی خود را متصل کند.\n\n"
"👉 برای ورود و استعلام سهمیه اختصاصی، لطفاً دستور /login را ارسال کنید یا از منوی /account وارد شوید."
)
else:
return (
"⚠️ AGY Account Not Connected!\n\n"
"Shared accounts are disabled. Please log in with your own Google account using /login or /account."
)
env = auth_manager.get_user_env(user_id) if user_id else os.environ.copy()
usage_raw = ""
credits_raw = ""
try:
proc1 = await asyncio.create_subprocess_exec(
"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()
proc2 = await asyncio.create_subprocess_exec(
"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()
except Exception as e:
logger.error(f"Failed to query AGY quota: {e}")
usage_raw = f"Error: {e}"
parsed = parse_agy_usage_output(usage_raw, credits_raw)
g_5h = parsed["gemini_5h"]
g_wk = parsed["gemini_weekly"]
c_5h = parsed["claude_5h"]
c_wk = parsed["claude_weekly"]
credits_val = parsed["credits"]
if g_5h is None and g_wk is None and c_5h is None:
# Fallback to pre-formatted raw output if parsing failed
if is_fa:
return (
"📊 گزارش لحظهای سهمیه و وضعیت مصرف AGY\n\n"
f"
{usage_raw}\n\n{credits_raw}\n\n"
"💡 سهمیههای ۵ ساعته و هفتگی به صورت خودکار تمدید میشوند."
)
else:
return (
"📊 Real-time AGY Quota & Usage Report\n\n"
f"{usage_raw}\n\n{credits_raw}\n\n"
"💡 Quotas are reset on 5-hour and 7-day windows."
)
if is_fa:
lines = [
"📈 گزارش وضعیت سهمیه و مصرف هوش مصنوعی (AGY)",
"",
"✨ ۱. مدلهای Gemini (جمینای):",
]
if g_5h is not None:
bar_5h = make_quota_bar(g_5h)
reset_5h = parse_iso_time_remaining(parsed["gemini_5h_reset"], is_fa=True)
lines.extend([
f" ⏳ سهمیه ۵ ساعته:",
f" {bar_5h} {g_5h}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_5h}",
])
if g_wk is not None:
bar_wk = make_quota_bar(g_wk)
reset_wk = parse_iso_time_remaining(parsed["gemini_weekly_reset"], is_fa=True)
lines.extend([
f" 📅 سهمیه هفتگی:",
f" {bar_wk} {g_wk}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_wk}",
])
lines.extend([
"",
"🌟 ۲. مدلهای Claude و GPT:",
]
)
if c_5h is not None:
bar_c5 = make_quota_bar(c_5h)
reset_c5 = parse_iso_time_remaining(parsed["claude_5h_reset"], is_fa=True)
lines.extend([
f" ⏳ سهمیه ۵ ساعته:",
f" {bar_c5} {c_5h}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_c5}",
])
if c_wk is not None:
bar_cwk = make_quota_bar(c_wk)
reset_cwk = parse_iso_time_remaining(parsed["claude_weekly_reset"], is_fa=True)
lines.extend([
f" 📅 سهمیه هفتگی:",
f" {bar_cwk} {c_wk}% باقیمانده",
f" • 🔄 زمان تمدید: {reset_cwk}",
])
lines.extend([
"",
f"🪙 اعتبار مدلها (Model Credits): {credits_val}",
"",
"💡 سهمیههای مصرف به صورت خودکار در بازههای زمانی فوق بازنشانی میشوند."
])
else:
lines = [
"📈 Real-Time AGY Quota & Usage Report",
"",
"✨ 1. Gemini Models:",
]
if g_5h is not None:
bar_5h = make_quota_bar(g_5h)
reset_5h = parse_iso_time_remaining(parsed["gemini_5h_reset"], is_fa=False)
lines.extend([
f" ⏳ 5-Hour Limit:",
f" {bar_5h} {g_5h}% Remaining",
f" • 🔄 Reset: {reset_5h}",
])
if g_wk is not None:
bar_wk = make_quota_bar(g_wk)
reset_wk = parse_iso_time_remaining(parsed["gemini_weekly_reset"], is_fa=False)
lines.extend([
f" 📅 Weekly Limit:",
f" {bar_wk} {g_wk}% Remaining",
f" • 🔄 Reset: {reset_wk}",
])
lines.extend([
"",
"🌟 2. Claude & GPT Models:",
])
if c_5h is not None:
bar_c5 = make_quota_bar(c_5h)
reset_c5 = parse_iso_time_remaining(parsed["claude_5h_reset"], is_fa=False)
lines.extend([
f" ⏳ 5-Hour Limit:",
f" {bar_c5} {c_5h}% Remaining",
f" • 🔄 Reset: {reset_c5}",
])
if c_wk is not None:
bar_cwk = make_quota_bar(c_wk)
reset_cwk = parse_iso_time_remaining(parsed["claude_weekly_reset"], is_fa=False)
lines.extend([
f" 📅 Weekly Limit:",
f" {bar_cwk} {c_wk}% Remaining",
f" • 🔄 Reset: {reset_cwk}",
])
lines.extend([
"",
f"🪙 Model Credits: {credits_val}",
"",
"💡 Quotas automatically reset periodically in the windows shown above."
])
return "\n".join(lines)