Spaces:
Sleeping
Sleeping
| """ | |
| البوت الرئيسي - مساعد 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() | |