Ejdjdososs's picture
Upload app.py with huggingface_hub
e5a00e1 verified
Raw
History Blame Contribute Delete
43.1 kB
"""
البوت الرئيسي - مساعد Z.ai على Telegram
مستضاف على Hugging Face Spaces
"""
import asyncio
import logging
import os
import sys
import time
from typing import Optional
from telegram import Update, BotCommand, constants
from telegram.constants import ParseMode
from telegram.ext import (
Application,
ApplicationBuilder,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
from telegram.helpers import escape_markdown
# استيراد الوحدات المحلية
from config import config
from llm import zai_client, detect_language
from memory import memory
from github_tools import github_tools
from owner_priority import priority_processor
from utils import (
split_message, format_error, format_success, format_github_repos,
format_github_files, BOT_COMMANDS_AR, HELP_TEXT,
WELCOME_OWNER_TEXT, WELCOME_USER_TEXT,
)
# القدرات المتقدمة
from code_executor import execute_python, format_execution_result
from file_generators import (
create_pdf, create_docx, create_xlsx, create_chart,
cleanup_file, run_in_executor,
)
from web_tools import (
search_duckduckgo, search_wikipedia, fetch_url_content,
format_search_results,
)
from image_tools import download_telegram_file, analyze_image
# ====== الإعدادات ======
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
stream=sys.stdout,
)
# تقليل ضجاج المكتبات
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("github").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# ====================================================================
# أوامر البوت
# ====================================================================
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/start - ترحيب"""
user = update.effective_user
await memory.register_user(
user_id=user.id,
username=user.username or "",
first_name=user.first_name or "",
)
if config.is_owner(user.id):
text = WELCOME_OWNER_TEXT
else:
text = WELCOME_USER_TEXT.format(first_name=user.first_name or "صديقي")
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/help - قائمة الأوامر"""
await update.message.reply_text(HELP_TEXT, parse_mode=ParseMode.MARKDOWN)
async def cmd_id(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/id - عرض معرّف المستخدم"""
user = update.effective_user
text = (
f"🆔 **معلوماتك**\n\n"
f"• User ID: `{user.id}`\n"
f"• Username: @{user.username or '—'}\n"
f"• Name: {user.first_name or '—'}\n"
)
if config.is_owner(user.id):
text += "\n👑 أنت المالك (أولوية قصوى)."
elif config.is_admin(user.id):
text += "\n🛡️ أنت مشرف."
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_mystatus(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/mystatus"""
user = update.effective_user
await memory.register_user(user.id, user.username or "", user.first_name or "")
if config.is_owner(user.id):
status = "👑 المالك (أولوية قصوى)"
elif config.is_admin(user.id):
status = "🛡️ مشرف"
else:
status = "👤 مستخدم عادي"
history = await memory.get_history(user.id)
text = (
f"📊 **حالتك**\n\n"
f"• الصلاحية: {status}\n"
f"• رسائل في الذاكرة: {len(history)}\n"
)
if config.is_owner(user.id) or config.is_admin(user.id):
text += "• GitHub: ✅ متاح" if github_tools.enabled else "• GitHub: ❌ غير مفعّل"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_reset(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/reset - مسح الذاكرة"""
user = update.effective_user
count = await memory.clear_history(user.id)
text = format_success(f"تم مسح {count} رسالة من ذاكرتك.")
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_models(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/models - عرض النماذج"""
available = await zai_client.list_available_models()
if available:
text = "📋 **النماذج المتاحة في حسابك:**\n\n"
for m in available:
marker = " ✅" if m == config.DEFAULT_MODEL else ""
text += f"• `{m}`{marker}\n"
else:
# عرض القوائم من الإعدادات
text = "📋 **النماذج المُعدّة:**\n\n"
text += f"• افتراضي: `{config.DEFAULT_MODEL}`\n"
text += f"• بدلاء: {', '.join('`'+m+'`' for m in config.FALLBACK_MODELS)}\n\n"
text += "_ملاحظة: لم أتمكن من جلب قائمة النماذج الفعلية من API._"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_model(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/model - عرض أو تغيير النموذج"""
user = update.effective_user
if not ctx.args:
current = await memory.get_preferred_model(user.id) or config.DEFAULT_MODEL
text = f"📦 **النموذج الحالي:** `{current}`\n\nلتغييره: `/model <name>`"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
return
new_model = ctx.args[0].strip()
await memory.set_preferred_model(user.id, new_model)
await update.message.reply_text(
format_success(f"تم تغيير النموذج إلى `{new_model}`"),
parse_mode=ParseMode.MARKDOWN,
)
async def cmd_lang(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/lang - تغيير اللغة"""
user = update.effective_user
if not ctx.args or ctx.args[0].lower() not in ("ar", "en"):
await update.message.reply_text(
"الاستخدام: `/lang ar` أو `/lang en`",
parse_mode=ParseMode.MARKDOWN,
)
return
lang = ctx.args[0].lower()
await memory.set_user_language(user.id, lang)
msg = "✅ تم ضبط اللغة على العربية." if lang == "ar" else "✅ Language set to English."
await update.message.reply_text(msg)
async def cmd_stats(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/stats - إحصائيات (للمالك فقط)"""
user = update.effective_user
if not config.is_admin(user.id):
await update.message.reply_text("هذا الأمر للمشرفين فقط.")
return
stats = await memory.get_stats()
opt_stats = optimizer.get_stats() if optimizer else {}
text = (
f"📊 **إحصائيات البوت**\n\n"
f"• إجمالي المستخدمين: {stats['users']}\n"
f"• إجمالي الرسائل: {stats['messages']}\n"
f"• ملاك مضبوطون: {stats['owners_configured']}\n"
f"• GitHub: {'✅' if github_tools.enabled else '❌'}\n"
f"• LLM: {'✅' if config.is_llm_configured else '❌'}\n"
f"• المزودون: {', '.join(config.configured_providers)}\n"
f"• المالك مضبوط: {'✅' if config.is_owner_configured else '❌'}\n"
)
if opt_stats:
text += (
f"\n💡 **التحسينات:**\n"
f"• Cache hits: {opt_stats.get('cache_hits', 0)}\n"
f"• API calls saved: {opt_stats.get('api_calls_saved', 0)}\n"
f"• Tokens saved: {opt_stats.get('tokens_saved', 0):,}\n"
)
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_usage(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/usage - استهلاكك اليومي"""
user = update.effective_user
if optimizer:
usage = optimizer.get_user_usage(user.id)
limit = config.OWNER_DAILY_TOKEN_LIMIT if config.is_owner(user.id) else config.USER_DAILY_TOKEN_LIMIT
percent = (usage["tokens_used"] / limit * 100) if limit else 0
text = (
f"📊 **استهلاكك اليوم**\n\n"
f"• التوكنات: {usage['tokens_used']:,} / {limit:,}\n"
f"• النسبة: {percent:.1f}%\n"
f"• الطلبات: {usage['requests']}\n"
f"• تُعاد الميزانية خلال: {usage['reset_in_hours']} ساعة\n"
)
else:
text = "نظام التحسينات غير مفعّل."
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
async def cmd_providers(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/providers - عرض حالة مزودي LLM"""
user = update.effective_user
if not config.is_admin(user.id):
await update.message.reply_text("هذا الأمر للمشرفين فقط.")
return
if multi_llm:
stats = multi_llm.get_stats()
text = "🔌 **مزودو LLM**\n\n"
for name, p_stats in stats.get("providers", {}).items():
h = stats.get("health", {}).get(name, {})
status = "✅" if h.get("available") else f"⏳ {h.get('cooldown_remaining', 0)}s"
text += (
f"**{name}** {status}\n"
f" calls: {p_stats['calls']} | success: {p_stats['success']}\n"
)
text += f"\n**الإجمالي:** {stats.get('total_providers', 0)} مزود، {stats.get('active_providers', 0)} نشط"
else:
text = "نظام متعدد المزودين غير مفعّل."
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
# ====================================================================
# أوامر GitHub
# ====================================================================
async def cmd_github(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/github - عمليات GitHub الكاملة"""
user = update.effective_user
if not config.is_admin(user.id):
await update.message.reply_text("❌ أوامر GitHub للمشرفين فقط.")
return
if not github_tools.enabled:
await update.message.reply_text(
format_error("GitHub غير مفعّل. أضف GITHUB_TOKEN كـ Secret.")
)
return
if not ctx.args:
await update.message.reply_text(
"الاستخدام: `/github help`\n"
"أوامر: me, repos, create, delete, files, read, write, "
"branches, commits, issue, issues, fork",
parse_mode=ParseMode.MARKDOWN,
)
return
sub = ctx.args[0].lower()
args = ctx.args[1:]
try:
if sub == "help":
await update.message.reply_text(
"**أوامر GitHub:**\n\n"
"• `/github me` - معلومات حسابك\n"
"• `/github repos` - سرد repos\n"
"• `/github create <name> [desc]` - إنشاء repo\n"
"• `/github delete <owner/repo>` - حذف repo\n"
"• `/github files <owner/repo> [path]` - سرد الملفات\n"
"• `/github read <owner/repo> <path>` - قراءة ملف\n"
"• `/github write <owner/repo> <path> <content>` - كتابة ملف\n"
"• `/github branches <owner/repo>` - الفروع\n"
"• `/github commits <owner/repo>` - آخر commits\n"
"• `/github issue <owner/repo> <title> | <body>` - فتح issue\n"
"• `/github issues <owner/repo>` - سرد issues\n"
"• `/github fork <owner/repo>` - fork repo",
parse_mode=ParseMode.MARKDOWN,
)
elif sub == "me":
info = await asyncio.to_thread(github_tools.get_user_info)
text = (
f"👤 **حسابك على GitHub**\n\n"
f"• Login: `{info['login']}`\n"
f"• Name: {info['name'] or '—'}\n"
f"• Email: {info['email'] or '—'}\n"
f"• Public repos: {info['public_repos']}\n"
f"• Followers: {info['followers']}\n"
f"• Bio: {info['bio'] or '—'}\n"
)
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
elif sub == "repos":
repos = await asyncio.to_thread(github_tools.list_repos, 20)
await update.message.reply_text(
format_github_repos(repos), parse_mode=ParseMode.MARKDOWN
)
elif sub == "create":
if not args:
await update.message.reply_text("الاستخدام: `/github create <name> [desc]`", parse_mode=ParseMode.MARKDOWN)
return
name = args[0]
desc = " ".join(args[1:]) if len(args) > 1 else ""
result = await asyncio.to_thread(
github_tools.create_repo, name, desc, True, True
)
if result.get("created"):
await update.message.reply_text(
format_success(f"تم إنشاء repo: {result['url']}"),
parse_mode=ParseMode.MARKDOWN,
)
else:
await update.message.reply_text(
format_error(str(result.get("error", "خطأ غير معروف")))
)
elif sub == "delete":
if not args:
await update.message.reply_text("الاستخدام: `/github delete <owner/repo>`", parse_mode=ParseMode.MARKDOWN)
return
result = await asyncio.to_thread(github_tools.delete_repo, args[0])
if result.get("deleted"):
await update.message.reply_text(format_success(f"تم حذف {args[0]}"))
else:
await update.message.reply_text(format_error(str(result.get("error", ""))))
elif sub == "files":
if not args:
await update.message.reply_text(
"الاستخدام: `/github files <owner/repo> [path]`",
parse_mode=ParseMode.MARKDOWN,
)
return
repo = args[0]
path = args[1] if len(args) > 1 else ""
files = await asyncio.to_thread(github_tools.list_files, repo, path)
await update.message.reply_text(
format_github_files(files), parse_mode=ParseMode.MARKDOWN
)
elif sub == "read":
if len(args) < 2:
await update.message.reply_text(
"الاستخدام: `/github read <owner/repo> <path>`",
parse_mode=ParseMode.MARKDOWN,
)
return
result = await asyncio.to_thread(github_tools.read_file, args[0], args[1])
if "error" in result:
await update.message.reply_text(format_error(result["error"]))
else:
content = result.get("content", "")
# إرسال ككتلة كود
msg = f"📄 **{result.get('path', '')}**\n({result.get('size', 0)} bytes)\n\n```\n{content}\n```"
for part in split_message(msg):
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
elif sub == "write":
if len(args) < 3:
await update.message.reply_text(
"الاستخدام: `/github write <owner/repo> <path> <content...>`",
parse_mode=ParseMode.MARKDOWN,
)
return
repo = args[0]
path = args[1]
content = " ".join(args[2:])
result = await asyncio.to_thread(
github_tools.update_file, repo, path, content, "Update via Telegram bot"
)
if not result.get("updated"):
# ربما الملف غير موجود - جرّب إنشاء
result = await asyncio.to_thread(
github_tools.create_file, repo, path, content, "Create via Telegram bot"
)
if result.get("updated") or result.get("created"):
await update.message.reply_text(
format_success(f"تم حفظ {path} في {repo}")
)
else:
await update.message.reply_text(format_error(str(result.get("error", ""))))
elif sub == "branches":
if not args:
await update.message.reply_text("الاستخدام: `/github branches <owner/repo>`", parse_mode=ParseMode.MARKDOWN)
return
branches = await asyncio.to_thread(github_tools.list_branches, args[0])
text = "🌿 **الفروع:**\n\n"
for b in branches:
prot = " 🔒" if b.get("protected") else ""
text += f"• `{b['name']}`{prot}\n"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
elif sub == "commits":
if not args:
await update.message.reply_text("الاستخدام: `/github commits <owner/repo>`", parse_mode=ParseMode.MARKDOWN)
return
commits = await asyncio.to_thread(github_tools.list_commits, args[0], 10)
text = "📜 **آخر commits:**\n\n"
for c in commits:
text += f"• `{c['sha']}` - {c['message']}\n _{c['author']}, {c['date']}_\n"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
elif sub == "issue":
if len(args) < 2:
await update.message.reply_text(
"الاستخدام: `/github issue <owner/repo> <title> | <body>`",
parse_mode=ParseMode.MARKDOWN,
)
return
repo = args[0]
rest = " ".join(args[1:])
if "|" in rest:
title, body = rest.split("|", 1)
title, body = title.strip(), body.strip()
else:
title, body = rest.strip(), ""
result = await asyncio.to_thread(
github_tools.create_issue, repo, title, body
)
if result.get("created"):
await update.message.reply_text(
format_success(f"Issue #{result['number']} مفتوح: {result['url']}")
)
else:
await update.message.reply_text(format_error(str(result.get("error", ""))))
elif sub == "issues":
if not args:
await update.message.reply_text("الاستخدام: `/github issues <owner/repo>`", parse_mode=ParseMode.MARKDOWN)
return
issues = await asyncio.to_thread(github_tools.list_issues, args[0])
text = "🐛 **Issues:**\n\n"
for i in issues:
text += f"• #{i['number']} {i['title']} [{i['state']}]\n {i['url']}\n"
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN)
elif sub == "fork":
if not args:
await update.message.reply_text("الاستخدام: `/github fork <owner/repo>`", parse_mode=ParseMode.MARKDOWN)
return
result = await asyncio.to_thread(github_tools.fork_repo, args[0])
if result.get("forked"):
await update.message.reply_text(
format_success(f"Forked: {result['url']}")
)
else:
await update.message.reply_text(format_error(str(result.get("error", ""))))
else:
await update.message.reply_text(
f"أمر غير معروف: `{sub}`. استخدم `/github help`."
)
except Exception as e:
logger.error(f"GitHub command failed: {e}", exc_info=True)
await update.message.reply_text(format_error(f"خطأ في تنفيذ أمر GitHub: {e}"))
# ====================================================================
# أوامر القدرات المتقدمة: كود، ملفات، مخططات، بحث، صور
# ====================================================================
async def cmd_code(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/code - تنفيذ كود Python"""
user = update.effective_user
# الكود يكون بعد الأمر مباشرة (سطر جديد أو نص)
raw = update.message.text
# إزالة /code من البداية
if "\n" in raw:
code_text = raw.split("\n", 1)[1]
else:
code_text = raw.replace("/code", "", 1).strip()
if not code_text:
await update.message.reply_text(
"📌 **الاستخدام:**\n"
"أرسل الكود بعد الأمر، مثال:\n"
"```\n/code\nprint('Hello')\nfor i in range(5): print(i)\n```",
parse_mode=ParseMode.MARKDOWN,
)
return
await update.message.reply_text("⏳ جاري التنفيذ...")
result = await execute_python(code_text)
formatted = format_execution_result(result)
for part in split_message(formatted):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception:
await update.message.reply_text(part)
async def cmd_pdf(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/pdf - إنشاء PDF من نص"""
raw = update.message.text
if "\n" in raw:
content = raw.split("\n", 1)[1]
else:
content = raw.replace("/pdf", "", 1).strip()
if not content:
await update.message.reply_text(
"📌 **الاستخدام:**\n/pdf\nضع النص هنا...\n"
"أو: /pdf عنوان|محتوى المستند"
)
return
title = "مستند"
if "|" in content:
title, content = content.split("|", 1)
title = title.strip()
content = content.strip()
await update.message.reply_text(f"⏳ جاري إنشاء PDF: {title}")
try:
path = await run_in_executor(create_pdf, content, title)
with open(path, "rb") as f:
await update.message.reply_document(
document=f, filename=f"{title}.pdf",
caption=f"📄 {title}",
)
cleanup_file(path)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في إنشاء PDF: {e}"))
async def cmd_docx(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/docx - إنشاء Word"""
raw = update.message.text
if "\n" in raw:
content = raw.split("\n", 1)[1]
else:
content = raw.replace("/docx", "", 1).strip()
if not content:
await update.message.reply_text("📌 **الاستخدام:** /docx\nضع النص هنا...")
return
title = "مستند"
if "|" in content:
title, content = content.split("|", 1)
title = title.strip()
content = content.strip()
try:
path = await run_in_executor(create_docx, content, title)
with open(path, "rb") as f:
await update.message.reply_document(
document=f, filename=f"{title}.docx",
caption=f"📝 {title}",
)
cleanup_file(path)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في إنشاء Word: {e}"))
async def cmd_xlsx(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/xlsx - إنشاء Excel من بيانات CSV"""
raw = update.message.text
if "\n" in raw:
content = raw.split("\n", 1)[1]
else:
content = raw.replace("/xlsx", "", 1).strip()
if not content:
await update.message.reply_text(
"📌 **الاستخدام:**\n/xlsx\ncol1,col2,col3\n1,2,3\n4,5,6"
)
return
# تحويل CSV إلى list of lists
import csv as csv_mod
rows = list(csv_mod.reader(content.split("\n")))
if not rows:
await update.message.reply_text(format_error("لا توجد بيانات صالحة."))
return
try:
path = await run_in_executor(create_xlsx, rows, "Sheet1", "بيانات")
with open(path, "rb") as f:
await update.message.reply_document(
document=f, filename="data.xlsx",
caption=f"📊 {len(rows)} صف",
)
cleanup_file(path)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في إنشاء Excel: {e}"))
async def cmd_chart(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/chart - إنشاء مخطط"""
raw = update.message.text
if "\n" in raw:
content = raw.split("\n", 1)[1]
else:
content = raw.replace("/chart", "", 1).strip()
if not content:
await update.message.reply_text(
"📌 **الاستخدام:**\n"
"```\n/chart bar|عنوان المخطط|تسمية1,تسمية2,تسمية3|10,20,30\n```\n"
"الأنواع: bar, line, pie, scatter, histogram\n"
"scatter: /chart scatter|عنوان|1,2,3|4,5,6 (x|y)\n"
"histogram: /chart histogram|عنوان|1,2,3,4,5",
parse_mode=ParseMode.MARKDOWN,
)
return
parts = content.split("|")
if len(parts) < 4:
await update.message.reply_text(format_error("صيغة غير صحيحة. استخدم: نوع|عنوان|labels|values"))
return
chart_type = parts[0].strip().lower()
title = parts[1].strip()
labels_raw = parts[2].strip()
values_raw = parts[3].strip()
data = {}
try:
if chart_type in ("bar", "line", "pie"):
data["labels"] = [l.strip() for l in labels_raw.split(",")]
data["values"] = [float(v.strip()) for v in values_raw.split(",")]
elif chart_type == "scatter":
data["x"] = [float(x.strip()) for x in labels_raw.split(",")]
data["y"] = [float(y.strip()) for y in values_raw.split(",")]
elif chart_type == "histogram":
data["values"] = [float(v.strip()) for v in labels_raw.split(",")]
else:
await update.message.reply_text(format_error(f"نوع غير معروف: {chart_type}"))
return
except ValueError as e:
await update.message.reply_text(format_error(f"خطأ في الأرقام: {e}"))
return
try:
path = await run_in_executor(create_chart, chart_type, data, title)
with open(path, "rb") as f:
await update.message.reply_photo(
photo=f, caption=f"📈 {title} ({chart_type})"
)
cleanup_file(path)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في المخطط: {e}"))
async def cmd_search(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/search - بحث في الويب"""
query = " ".join(ctx.args).strip() if ctx.args else ""
if not query:
await update.message.reply_text("📌 **الاستخدام:** `/search كلمة البحث`", parse_mode=ParseMode.MARKDOWN)
return
await update.message.reply_text(f"🔍 أبحث عن: `{query}`", parse_mode=ParseMode.MARKDOWN)
try:
# بحث في DuckDuckGo
ddg_results = await search_duckduckgo(query, max_results=6)
# بحث في Wikipedia (عربي وإنجليزي)
wiki_ar = await search_wikipedia(query, lang="ar", max_results=2)
wiki_en = await search_wikipedia(query, lang="en", max_results=2)
text = ""
if ddg_results:
text += format_search_results(ddg_results, query)
if wiki_ar or wiki_en:
text += "\n📚 **ويكيبيديا:**\n\n"
for r in (wiki_ar + wiki_en)[:4]:
text += f"• [{r['title']}]({r['url']})\n _{r['snippet'][:150]}_\n"
if not text:
text = f"❌ لا توجد نتائج لـ: `{query}`"
for part in split_message(text):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception:
await update.message.reply_text(part)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في البحث: {e}"))
async def cmd_read_url(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/read <url> - استخراج محتوى صفحة ويب"""
if not ctx.args:
await update.message.reply_text("📌 **الاستخدام:** `/read https://example.com`", parse_mode=ParseMode.MARKDOWN)
return
url = ctx.args[0]
await update.message.reply_text(f"📄 أقرأ: {url}")
try:
result = await fetch_url_content(url, max_chars=4000)
if result.get("error"):
await update.message.reply_text(format_error(result["error"]))
return
text = f"📄 **{result['title']}**\n\n{result['content']}"
for part in split_message(text):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception:
await update.message.reply_text(part)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في القراءة: {e}"))
async def cmd_image_analyze(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""/vision <prompt> - تحليل صورة (يرد على صورة موجودة)"""
if not update.message.reply_to_message or not update.message.reply_to_message.photo:
await update.message.reply_text(
"📌 **الاستخدام:** أرسل صورة ثم رد عليها بـ `/vision ما الذي تراه؟`"
)
return
prompt = " ".join(ctx.args).strip() if ctx.args else "صف هذه الصورة بالتفصيل."
await update.message.reply_text("🖼️ جاري تحليل الصورة...")
try:
# أكبر صورة (آخر عنصر في photo list)
photo = update.message.reply_to_message.photo[-1]
image_bytes = await download_telegram_file(ctx.bot, photo.file_id)
result = await analyze_image(image_bytes, prompt, hf_token=config.HF_TOKEN)
for part in split_message(result):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception:
await update.message.reply_text(part)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في تحليل الصورة: {e}"))
async def handle_photo(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""معالجة الصور المرسلة مباشرة (بدون /vision) - تحليل تلقائي"""
user = update.effective_user
if not update.message.photo:
return
await memory.register_user(
user_id=user.id,
username=user.username or "",
first_name=user.first_name or "",
)
photo = update.message.photo[-1]
caption = update.message.caption or "صف هذه الصورة بالتفصيل."
await update.message.reply_text("🖼️ أرى صورة! جاري التحليل...")
try:
image_bytes = await download_telegram_file(ctx.bot, photo.file_id)
result = await analyze_image(image_bytes, caption, hf_token=config.HF_TOKEN)
for part in split_message(result):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception:
await update.message.reply_text(part)
except Exception as e:
await update.message.reply_text(format_error(f"خطأ في التحليل: {e}"))
# ====================================================================
# معالج الرسائل العادية - يمر عبر Priority Queue
# ====================================================================
async def process_chat_message(
user_id: int,
user_lang: str,
is_owner: bool,
text: str,
preferred_model: Optional[str],
) -> tuple:
"""المهمة الفعلية لمعالجة رسالة المستخدم - تُنفّذ داخل Priority Queue"""
# حفظ رسالة المستخدم
await memory.add_message(user_id, "user", text)
await memory.increment_message_count(user_id)
# جلب التاريخ
history = await memory.get_history(user_id)
# استدعاء GLM
reply, model_used = await zai_client.chat(
messages=history,
user_lang=user_lang,
is_owner=is_owner,
model=preferred_model,
)
# حفظ رد المساعد
await memory.add_message(user_id, "assistant", reply, model_used)
return reply, model_used
async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""معالج الرسائل النصية العادية"""
user = update.effective_user
text = update.message.text
if not text:
return
# تسجيل المستخدم
user_info = await memory.register_user(
user_id=user.id,
username=user.username or "",
first_name=user.first_name or "",
)
# التحقق من الإعدادات
if not config.is_llm_configured:
await update.message.reply_text(
format_error(
"البوت غير مُعد بشكل صحيح: ZAI_API_KEY مفقود.\n"
"أضفه كـ Secret في Hugging Face Space."
)
)
return
# كشف اللغة
detected = detect_language(text)
user_lang = await memory.get_user_language(user.id)
# إذا كانت رسالة المستخدم بلغة مختلفة عن المحفوظة - استخدم المكتشفة
if detected != "mixed":
user_lang = detected
preferred_model = await memory.get_preferred_model(user.id)
is_owner = config.is_owner(user.id)
# إرسال مؤشر "يكتب..."
typing_task = asyncio.create_task(_keep_typing(update))
# إرسال المهمة لطابور الأولوية
task_id = f"chat-{user.id}-{int(time.time()*1000)}"
try:
# المالك ينتظر بلا حد، المستخدم العادي له حد أطول
timeout = 180 if is_owner else 120
reply, model_used = await asyncio.wait_for(
priority_processor.submit(
user.id, # للأولوية
process_chat_message, # الدالة
user.id, # أول arg للدالة
user_lang,
is_owner,
text,
preferred_model,
task_id=task_id,
),
timeout=timeout,
)
except asyncio.TimeoutError:
typing_task.cancel()
await update.message.reply_text(
format_error("انتهت المهلة قبل إكمال الرد. حاول مرة أخرى.")
)
return
except Exception as e:
typing_task.cancel()
logger.error(f"Chat failed: {e}", exc_info=True)
await update.message.reply_text(
format_error(f"حدث خطأ أثناء المعالجة: {e}")
)
return
typing_task.cancel()
# إرسال الرد (مقسّم إذا كان طويلاً)
parts = split_message(reply)
for i, part in enumerate(parts):
try:
await update.message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
except Exception as e:
# إذا فشل Markdown، أعد الإرسال كنص عادي
logger.warning(f"Markdown send failed, retrying as plain text: {e}")
try:
await update.message.reply_text(part)
except Exception as e2:
logger.error(f"Final send failed: {e2}")
# تأخير بسيط بين الرسائل لتجنب rate limiting
if i < len(parts) - 1:
await asyncio.sleep(0.3)
async def _keep_typing(update: Update):
"""إبقاء مؤشر "يكتب..." نشطاً"""
try:
while True:
try:
await update.effective_chat.send_action(constants.ChatAction.TYPING)
except Exception:
pass
await asyncio.sleep(4)
except asyncio.CancelledError:
pass
# ====================================================================
# إعداد البوت وتشغيله
# ====================================================================
async def post_init(app: Application):
"""يُستدعى بعد بناء التطبيق وقبل البدء"""
await memory.init()
await priority_processor.start()
# ضبط أوامر البوت
commands = [BotCommand(c[0], c[1]) for c in BOT_COMMANDS_AR]
await app.bot.set_my_commands(commands)
logger.info("Bot initialized successfully")
# فحص الإعدادات
issues = []
if not config.TELEGRAM_BOT_TOKEN:
issues.append("TELEGRAM_BOT_TOKEN مفقود")
if not config.ZAI_API_KEY:
issues.append("ZAI_API_KEY مفقود - البوت لن يعمل")
else:
keys_count = len(config.all_zai_keys)
logger.info(f"🔢 Z.ai keys available: {keys_count} (load balancing)")
if not config.GITHUB_TOKEN:
issues.append("GITHUB_TOKEN مفقود (GitHub معطّل)")
if not config.HF_TOKEN:
issues.append("HF_TOKEN مفقود (تحليل الصور بـ HF معطّل)")
if not config.is_owner_configured:
issues.append("OWNER_ID غير مضبوط (لن تعمل الأولوية)")
if issues:
logger.warning("⚠️ Configuration issues:\n" + "\n".join(f" - {i}" for i in issues))
else:
logger.info("✅ All configuration checks passed")
async def on_shutdown(app: Application):
"""تنظيف عند الإيقاف"""
await priority_processor.stop()
await zai_client.close()
logger.info("Bot shutdown complete")
def main():
"""نقطة الدخول - webhook mode باستخدام run_webhook المدمج"""
if not config.TELEGRAM_BOT_TOKEN:
logger.error("TELEGRAM_BOT_TOKEN غير مضبوط! خروج.")
sys.exit(1)
import time as _t
import httpx
PORT = 7860
WEBHOOK_PATH = "/webhook"
# بناء التطبيق
app = (
ApplicationBuilder()
.token(config.TELEGRAM_BOT_TOKEN)
.post_init(post_init)
.post_shutdown(on_shutdown)
.concurrent_updates(True)
.connect_timeout(60.0)
.read_timeout(60.0)
.write_timeout(60.0)
.pool_timeout(60.0)
.build()
)
# تسجيل المعالجات
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_help))
app.add_handler(CommandHandler("id", cmd_id))
app.add_handler(CommandHandler("mystatus", cmd_mystatus))
app.add_handler(CommandHandler("reset", cmd_reset))
app.add_handler(CommandHandler("models", cmd_models))
app.add_handler(CommandHandler("model", cmd_model))
app.add_handler(CommandHandler("lang", cmd_lang))
app.add_handler(CommandHandler("stats", cmd_stats))
app.add_handler(CommandHandler("usage", cmd_usage))
app.add_handler(CommandHandler("providers", cmd_providers))
app.add_handler(CommandHandler("github", cmd_github))
app.add_handler(CommandHandler("code", cmd_code))
app.add_handler(CommandHandler("pdf", cmd_pdf))
app.add_handler(CommandHandler("docx", cmd_docx))
app.add_handler(CommandHandler("xlsx", cmd_xlsx))
app.add_handler(CommandHandler("chart", cmd_chart))
app.add_handler(CommandHandler("search", cmd_search))
app.add_handler(CommandHandler("read", cmd_read_url))
app.add_handler(CommandHandler("vision", cmd_image_analyze))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
# ضبط الـ webhook يدوياً من هنا (قبل بدء البوت)
# هذا يضمن أن Telegram يعرف مكان إرسال التحديثات
WEBHOOK_URL = f"https://Ejdjdososs-zai-telegram-bot.hf.space{WEBHOOK_PATH}"
logger.info(f"📡 Setting webhook to {WEBHOOK_URL}...")
def set_webhook_with_retry():
for attempt in range(5):
try:
resp = httpx.post(
f"https://api.telegram.org/bot{config.TELEGRAM_BOT_TOKEN}/setWebhook",
json={
"url": WEBHOOK_URL,
"allowed_updates": [
"message", "edited_message", "callback_query",
"inline_query", "chosen_inline_result",
],
"max_connections": 40,
"drop_pending_updates": False,
},
timeout=30,
)
data = resp.json()
if data.get("ok"):
logger.info(f"✅ Webhook set successfully")
return True
else:
logger.error(f"❌ Webhook set failed: {data}")
except Exception as e:
logger.error(f"❌ Webhook attempt {attempt+1} failed: {e}")
_t.sleep(5)
return False
# ضبط الـ webhook قبل البدء
set_webhook_with_retry()
# بدء التطبيق في webhook mode
# url_path فقط - لا webhook_url (لا يحاول setWebhook تلقائياً)
logger.info(f"🚀 Starting webhook server on port {PORT} at path {WEBHOOK_PATH}")
max_retries = 5
for attempt in range(1, max_retries + 1):
try:
logger.info(f"🎯 Attempt {attempt}/{max_retries}")
app.run_webhook(
listen="0.0.0.0",
port=PORT,
url_path=WEBHOOK_PATH,
# بدون webhook_url - لا يتصل بـ Telegram لضبط الـ webhook
# الـ webhook ضُبط يدوياً أعلاه
drop_pending_updates=False,
# إعدادات HTTP server
web_url=None,
)
break
except Exception as e:
logger.error(f"❌ Attempt {attempt} failed: {e}", exc_info=True)
if attempt < max_retries:
logger.info("⏳ Waiting 15s before retry...")
_t.sleep(15)
else:
logger.error("🛑 All retries exhausted.")
while True:
_t.sleep(3600)
if __name__ == "__main__":
main()