Spaces:
Runtime error
Runtime error
| import sys | |
| import subprocess | |
| import os | |
| import importlib | |
| import asyncio | |
| import re | |
| import time | |
| import shutil | |
| import json | |
| import zipfile | |
| import urllib.parse | |
| import random | |
| import io | |
| import threading | |
| from datetime import datetime, timedelta | |
| # ================================ | |
| # 🛠️ 1. Auto-Installer & Dependencies | |
| # ================================ | |
| def install_dependencies(): | |
| packages = [ | |
| ("discord.py", "discord"), | |
| ("google-generativeai", "google.generativeai"), | |
| ("aiohttp", "aiohttp"), | |
| ("nest_asyncio", "nest_asyncio"), | |
| ("python-docx", "docx"), | |
| ("patool", "patoolib"), | |
| ("Pillow", "PIL"), | |
| ("bs4", "bs4"), | |
| ("requests", "requests"), | |
| ("cloudscraper", "cloudscraper"), | |
| ("selenium", "selenium"), | |
| ("webdriver-manager", "webdriver_manager"), | |
| ("gdown", "gdown"), | |
| ("flask", "flask") | |
| ] | |
| try: subprocess.run(["unrar"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| except: pass | |
| try: | |
| subprocess.run("apt-get update && apt-get install -y wget gnupg && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list' && apt-get update && apt-get install -y google-chrome-stable", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| except: pass | |
| for pkg, import_name in packages: | |
| try: importlib.import_module(import_name) | |
| except ImportError: | |
| try: subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--disable-pip-version-check", "-U", pkg]) | |
| except: pass | |
| install_dependencies() | |
| import discord | |
| from discord.ext import commands, tasks | |
| from discord import app_commands | |
| from discord.ui import Select, View, Button, Modal, TextInput | |
| import google.generativeai as genai | |
| import aiohttp | |
| import nest_asyncio | |
| from docx import Document | |
| import patoolib | |
| from PIL import Image, ImageDraw, ImageFont, ImageFile | |
| from bs4 import BeautifulSoup | |
| import requests | |
| import cloudscraper | |
| import gdown | |
| from flask import Flask | |
| from threading import Thread | |
| from selenium import webdriver | |
| from selenium.webdriver.chrome.service import Service | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from selenium.webdriver.support.ui import WebDriverWait | |
| from selenium.webdriver.support import expected_conditions as EC | |
| from webdriver_manager.chrome import ChromeDriverManager | |
| nest_asyncio.apply() | |
| Image.MAX_IMAGE_PIXELS = None | |
| ImageFile.LOAD_TRUNCATED_IMAGES = True | |
| # ================================ | |
| # 🌐 2. Keep-Alive Server (Hugging Face) | |
| # ================================ | |
| app = Flask('') | |
| def home(): | |
| return "Bot is running perfectly!" | |
| def run_server(): | |
| app.run(host='0.0.0.0', port=7860) | |
| def keep_alive(): | |
| t = Thread(target=run_server) | |
| t.start() | |
| # ================================ | |
| # ⚙️ 3. Config & Keys | |
| # ================================ | |
| # سيتم جلب التوكن والمفاتيح من الـ Secrets في Hugging Face للحماية | |
| DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") | |
| RAW_KEYS = os.getenv("GEMINI_KEYS", "") | |
| GEMINI_KEYS = [k.strip() for k in RAW_KEYS.split(",") if k.strip()] | |
| WELCOME_CHANNEL_ID = 1476271670934372433 | |
| AUTO_ROLE_ID = 1421231898277187734 | |
| LOG_CHANNEL_ID = 1476386506296787095 | |
| TRACKER_CHANNEL_ID = 1478415906614018191 | |
| BASE_DIR = os.getcwd() | |
| TEMP_ROOT = os.path.join(BASE_DIR, "bot_temp") | |
| USERS_DB_FILE = os.path.join(BASE_DIR, "users_db.json") | |
| TRACKER_DB_FILE = os.path.join(BASE_DIR, "tracker_cache.json") | |
| active_tasks = {} | |
| invites_cache = {} | |
| bot_queue = {"ocr": [], "merge": []} | |
| intents = discord.Intents.all() | |
| bot = commands.Bot(command_prefix="!", intents=intents) | |
| def natural_sort_key(s): | |
| return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)] | |
| # ================================ | |
| # 🧠 4. Gemini Smart Manager (The Core Engine) | |
| # ================================ | |
| class GeminiManager: | |
| def __init__(self): | |
| self.keys = GEMINI_KEYS | |
| self.cooldowns = {key: 0 for key in self.keys} | |
| self.lock = threading.Lock() | |
| def get_valid_key(self): | |
| with self.lock: | |
| current_time = time.time() | |
| available_keys = [k for k, cooldown in self.cooldowns.items() if current_time > cooldown] | |
| if not available_keys: | |
| return None | |
| return random.choice(available_keys) | |
| def report_dead_key(self, key): | |
| with self.lock: | |
| self.cooldowns[key] = time.time() + 300 | |
| print(f" مفتاح تم حظره لمدة خمساية ن: {key[:10]}...") | |
| async def analyze_image(self, image_path, advance_mode=False): | |
| prompt = """TASK: STRICT OCR ONLY (VISION + COMPLETENESS ENFORCED) | |
| ================================================== | |
| CORE PRINCIPLES (NON-NEGOTIABLE) | |
| ================================================== | |
| - VISUAL SHAPE ONLY. | |
| - NO inference, NO guessing, NO hallucination. | |
| - Language, meaning, emotion, size, and layout are IRRELEVANT. | |
| - Extract ONLY what is visibly present on the page. | |
| - COMPLETE the current page BEFORE moving to the next. | |
| ================================================== | |
| ORDER NO. 1: ABSOLUTE EXTRACTION RULES | |
| ================================================== | |
| 1. NO TRANSLATION: Output the EXACT original text only. | |
| 2. NO HALLUCINATION: DO NOT invent, guess, infer, or autocomplete text. If text is NOT clearly visible → DO NOT output it. | |
| 3. NO OMISSION: ALL visible text on the page MUST be extracted. Page is considered COMPLETE only after extracting: Every bubble, Every box, Every small text, Every SFX, Every background text. | |
| 4. NO REPETITION: Do NOT repeat characters endlessly. Use condensed form only. | |
| ================================================== | |
| ORDER NO. 2: ONE ELEMENT = ONE LINE (ABSOLUTE LOCK & GROUPING RULES) | |
| ================================================== | |
| - BUBBLES & BOXES: A bubble/box is ONE continuous visible border. Multiple visual lines inside ONE border MUST be merged into EXACTLY ONE output line. Replace internal line breaks with a single space. | |
| - BORDERLESS TEXT (OT) & SYSTEM SCREENS (<>): Group contiguous or visually adjacent text lines that belong to the same paragraph or logical block into EXACTLY ONE output line. Do NOT output each physical line as a separate tag. | |
| - NEVER merge bubbles vertically, horizontally, diagonally. Fragment output is FORBIDDEN. | |
| ================================================== | |
| ORDER NO. 3: SENTENCE INTEGRITY | |
| ================================================== | |
| - A single sentence MUST stay in ONE tag. NEVER continue a sentence using OT: or any other tag. | |
| ================================================== | |
| ORDER NO. 4: TAG SELECTION (SHAPE-ONLY) | |
| ================================================== | |
| Tag selection is STRICTLY based on the VISUAL SHAPE of the bubble's border. | |
| 1. (): THOUGHT (STRICT IDENTIFICATION) | |
| - Visual MUST BE EXACTLY ONE OF THESE TWO: | |
| A) Connected to the character by a DOTTED TAIL (a trail of small distinct circles). | |
| B) The "LION'S MANE" / "FURRY" border: The border is made of very dense, ultra-thin, fine black lines radiating outward evenly around the entire bubble. | |
| - Format: `(): Text` | |
| 2. %: SCREAM / SHOUT / PAIN | |
| - Visual: LARGE, IRREGULAR jagged zig-zags, explosion-like shapes, or violently shaky borders. | |
| - CRITICAL NEGATIVE RULE: Do NOT use `%` if the border consists of fine, dense, uniform, hair-like lines (Lion's Mane). That MUST be `()`. | |
| - Format: `%: Text` | |
| 3. ST: SMALL TEXT | |
| - Visibly smaller text, floating freely, annotations, or outside any main bubble. | |
| - CRITICAL: Do NOT default to `""` for text that is clearly smaller than the main dialogue. It MUST be `ST`. | |
| - Format: `ST: Text` | |
| 4. "": SPEECH | |
| - Smooth oval or rectangular bubble with a normal solid speech tail. | |
| - Format: `"": Text` | |
| 5. //: CONNECTED SPEECH | |
| - Visually connected continuation bubble. SAME speaker only. | |
| - Format: `//: Text` | |
| 6. []: BOX | |
| - Physical rectangular narration box, NOT a screen/UI. Multiple lines inside one box MUST be merged into one single `[]` line. | |
| - Format: `[]: Text` | |
| 7. <>: SCREEN SYSTEM | |
| - Text inside phone screen, game screen, system UI, HUD. Treat the entire screen text block as ONE single `<>` line. | |
| - Format: `<>: Text` | |
| 8. OT: ARTWORK / BACKGROUND TEXT | |
| - Large text written directly on artwork, NO bubble/box. Group adjacent lines together! | |
| - Format: `OT: Text` | |
| 9. SFX: SOUND EFFECTS | |
| - Stylized non-verbal sound lettering. | |
| - Format: `SFX: Text` | |
| ================================================== | |
| ORDER NO. 5: STRICT READING DIRECTION & SEQUENCE (CRITICAL) | |
| ================================================== | |
| 1. PRIMARY RULE (HEIGHT): ALWAYS read from TOP to BOTTOM. The element physically higher on the page comes first. | |
| 2. SECONDARY RULE (TIE-BREAKER): If two separate elements are at the exact same height, read RIGHT to LEFT. | |
| 3. CONNECTED BUBBLES (CHAIN RULE): When bubbles are physically touching or connected by tails, treat them as a flowing chain. You MUST visually trace the chain from the HIGHEST bubble down to the LOWEST. NEVER reverse the order of a connected sequence. The highest bubble is `""`, and the subsequent bubbles attached below it are `//:`. | |
| ================================================== | |
| ORDER NO. 6: FINAL ENFORCEMENT (ZERO TOLERANCE) | |
| ================================================== | |
| - NO translation, NO hallucination, NO missing text, NO merged bubbles, NO split bubbles. | |
| - ONE tag per line. ONE bubble per line. ALL text must be tagged. | |
| - STRICT sequence must be followed. | |
| - IF NO TEXT IS VISIBLE IN THE IMAGE, RETURN EXACTLY: `OT: [No Text]` | |
| """ | |
| safety_settings = [{"category": c, "threshold": "BLOCK_NONE"} for c in [ | |
| "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH", | |
| "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT" | |
| ]] | |
| gen_config = {"temperature": 0.0, "max_output_tokens": 8192, "top_p": 0.9} | |
| model_name = db.get_bot_setting("advance_model" if advance_mode else "normal_model") | |
| for attempt in range(len(self.keys)): | |
| current_api_key = self.get_valid_key() | |
| if not current_api_key: | |
| await asyncio.sleep(2) | |
| continue | |
| genai.configure(api_key=current_api_key) | |
| await asyncio.sleep(random.uniform(1.0, 2.5)) | |
| try: | |
| model = genai.GenerativeModel(model_name, safety_settings=safety_settings) | |
| with open(image_path, "rb") as f: img = f.read() | |
| response = await asyncio.wait_for( | |
| asyncio.to_thread( | |
| model.generate_content, | |
| [prompt, {"mime_type": "image/jpeg", "data": img}], | |
| generation_config=gen_config | |
| ), timeout=45.0 | |
| ) | |
| if response.text: return response.text | |
| except asyncio.TimeoutError: | |
| continue | |
| except Exception as e: | |
| error_msg = str(e).lower() | |
| if "not found" in error_msg or "404" in error_msg or "invalid" in error_msg: continue | |
| if "429" in error_msg or "quota" in error_msg or "exhausted" in error_msg: | |
| self.report_dead_key(current_api_key) | |
| await asyncio.sleep(1.5) | |
| continue | |
| if "safety" in error_msg or "finish_reason" in error_msg: break | |
| continue | |
| return "" | |
| ai_engine = GeminiManager() | |
| # ================================ | |
| # 🧹 5. Clean Text (Anti-Hallucination Filter) | |
| # ================================ | |
| def clean_text(raw, settings): | |
| try: | |
| add_blank = settings.get("blank_line", True) | |
| sfx_on = settings.get("include_sfx", False) | |
| c = settings.get("custom_tags", {}) | |
| tag_map = {k: c.get(k, k) for k in ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%']} | |
| base_defaults = ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%'] | |
| all_tags = {d: d for d in base_defaults} | |
| for k, v in c.items(): all_tags[v] = k | |
| hallucination_triggers = [ | |
| "TASK: STRICT OCR", "CORE PRINCIPLES", "ORDER NO. 1", | |
| "ORDER NO. 2", "ORDER NO. 3", "ORDER NO. 4", "ORDER NO. 5", | |
| "ORDER NO. 6", "VISUAL SHAPE ONLY", "Format:" | |
| ] | |
| out = [] | |
| for line in raw.split('\n'): | |
| line = line.strip() | |
| if not line or re.match(r'^(I|II|III|IV|V|VI|VII|VIII|IX|X)-\s', line, re.IGNORECASE): continue | |
| if any(trigger.lower() in line.lower() for trigger in hallucination_triggers): continue | |
| if "panel" in line.lower() or "bubble" in line.lower(): continue | |
| line = re.sub(r'^(?:OT:\s*)?\[OT:\s*(.*?)\]?$', r'OT: \1', line) | |
| line = re.sub(r'^OT:\s*OT:\s*', r'OT: ', line) | |
| line = line.replace("<:>", "<>:").replace("< : >", "<>:") | |
| line = re.sub(r'(.)\1{6,}', r'\1\1...', line) | |
| line = line.replace("Million", "milyun").replace("million", "milyun") | |
| detected_tag = None | |
| content = line | |
| for tag_str in sorted(all_tags.keys(), key=len, reverse=True): | |
| if line.startswith(f"{tag_str}:") or line.startswith(tag_str): | |
| detected_tag = all_tags[tag_str] | |
| content = line[len(tag_str)+1:].strip() if line.startswith(f"{tag_str}:") else line[len(tag_str):].strip() | |
| break | |
| if detected_tag: | |
| if detected_tag == 'SFX' and not sfx_on: continue | |
| content = content.strip().strip('"').strip("'").strip("`").strip() | |
| if detected_tag == '()' and content.startswith('(') and content.endswith(')'): | |
| content = content[1:-1].strip() | |
| if len(content) > 600 and " " not in content: content = content[:50] + "..." | |
| final_tag = tag_map.get(detected_tag, detected_tag) | |
| out.append(f"{final_tag}: {content}") | |
| else: | |
| clean = line.strip().strip('"').strip("'").strip("`").strip() | |
| if clean: | |
| if clean.startswith(":") or clean.startswith(":"): | |
| clean_speech = clean[1:].strip() | |
| speech_tag = tag_map.get('""', '""') | |
| out.append(f"{speech_tag}: {clean_speech}") | |
| else: | |
| if out: | |
| out[-1] += f" {clean}" | |
| else: | |
| final_ot = tag_map.get('OT', 'OT') | |
| if not any(clean.startswith(v) for v in tag_map.values()): out.append(f"{final_ot}: {clean}") | |
| else: out.append(clean) | |
| final_out = [] | |
| for item in out: | |
| final_out.append(item) | |
| if add_blank: final_out.append("") | |
| return final_out | |
| except: return [raw] | |
| # ================================ | |
| # 🕹️ 6. UI Components (Modals & Views) | |
| # ================================ | |
| class CancelView(View): | |
| def __init__(self, user_id, task_id, total_pages): | |
| super().__init__(timeout=None) | |
| self.user_id = user_id | |
| self.task_id = task_id | |
| self.total_pages = total_pages | |
| async def stop(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| current_progress = active_tasks.get(f"{self.task_id}_progress", 0) | |
| if self.total_pages > 0 and (current_progress / self.total_pages) >= 0.5: | |
| await interaction.response.send_message("❌ لا يمكن الإلغاء الآن، تم الانتهاء من أكثر من 50% من العمل.", ephemeral=True) | |
| button.disabled = True | |
| await interaction.message.edit(view=self) | |
| return | |
| if self.task_id in active_tasks: | |
| active_tasks[self.task_id].cancel() | |
| del active_tasks[self.task_id] | |
| button.disabled = True | |
| button.label = "تم الإلغاء ⛔" | |
| await interaction.response.edit_message(view=self) | |
| await interaction.followup.send("⛔ تم إلغاء العملية بنجاح. لن يتم خصم المزيد.", ephemeral=True) | |
| class FeedbackModal(Modal, title="📝 تقييم جودة الاستخراج (Feedback)"): | |
| feedback_text = TextInput(label='رأيك في دقة النصوص وملاحظاتك', style=discord.TextStyle.paragraph, placeholder='اكتب مشكلتك أو اقتراحك هنا...', required=True, max_length=1000) | |
| async def on_submit(self, interaction: discord.Interaction): | |
| await interaction.response.send_message("✅ شكراً لتقييمك! تم إرسال ملاحظاتك للإدارة لتطوير البوت.", ephemeral=True) | |
| embed = discord.Embed(title="📝 تقييم جديد للاستخراج", color=0x3498db, timestamp=datetime.utcnow()) | |
| embed.add_field(name="المستخدم", value=interaction.user.mention, inline=False) | |
| embed.add_field(name="التقييم/المشكلة", value=self.feedback_text.value, inline=False) | |
| try: await send_log(interaction.guild, embed) | |
| except: pass | |
| class PostExtractView(View): | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def feedback(self, interaction: discord.Interaction, button: Button): | |
| await interaction.response.send_modal(FeedbackModal()) | |
| # ================================ | |
| # 🗄️ 7. Database & Auto-Backup System | |
| # ================================ | |
| class UserDatabase: | |
| def __init__(self): | |
| self.lock = threading.Lock() | |
| self.db = self.load_db() | |
| self.init_bot_config() | |
| def load_db(self): | |
| if os.path.exists(USERS_DB_FILE): | |
| try: return json.load(open(USERS_DB_FILE, 'r', encoding='utf-8')) | |
| except: return {} | |
| return {} | |
| def save_db(self): | |
| try: | |
| with open(USERS_DB_FILE, 'w', encoding='utf-8') as f: | |
| json.dump(self.db, f, indent=4, ensure_ascii=False) | |
| except: pass | |
| def init_bot_config(self): | |
| with self.lock: | |
| if "bot_config" not in self.db: | |
| self.db["bot_config"] = {"advance_model": "gemini-2.5-flash", "normal_model": "gemini-2.5-flash", "bot_admins": []} | |
| self.save_db() | |
| def is_bot_admin(self, user_id): | |
| return user_id in self.db.get("bot_config", {}).get("bot_admins", []) | |
| def add_bot_admin(self, user_id): | |
| with self.lock: | |
| if user_id not in self.db["bot_config"]["bot_admins"]: | |
| self.db["bot_config"]["bot_admins"].append(user_id) | |
| self.save_db() | |
| def get_bot_setting(self, key): | |
| with self.lock: | |
| self.db = self.load_db() | |
| return self.db.get("bot_config", {}).get(key, "gemini-2.5-flash") | |
| def update_bot_setting(self, key, value): | |
| with self.lock: | |
| self.db = self.load_db() | |
| if "bot_config" not in self.db: self.db["bot_config"] = {} | |
| self.db["bot_config"][key] = value | |
| self.save_db() | |
| def get_user(self, user_id, username): | |
| with self.lock: | |
| self.db = self.load_db() | |
| uid = str(user_id) | |
| if uid not in self.db: | |
| self.db[uid] = { | |
| "name": username, "points": 0.0, "registered": False, "rank": "Normal", | |
| "images_extracted": 0, "points_spent": 0.0, "free_merges": 3, | |
| "settings": {"format": "docx", "blank_line": True, "include_sfx": False, "language": "ar", "advance_mode": False, "custom_tags": {}} | |
| } | |
| self.save_db() | |
| return self.db[uid] | |
| def modify_points(self, user_id, amount, spent=False): | |
| with self.lock: | |
| self.db = self.load_db() | |
| uid = str(user_id) | |
| if uid in self.db: | |
| self.db[uid]["points"] = round(self.db[uid]["points"] + amount, 2) | |
| if spent and amount < 0: | |
| self.db[uid]["points_spent"] = round(self.db[uid].get("points_spent", 0) + abs(amount), 2) | |
| self.save_db() | |
| return self.db[uid]["points"] | |
| return 0 | |
| def add_stats(self, user_id, images_count): | |
| with self.lock: | |
| uid = str(user_id) | |
| if uid in self.db: | |
| self.db[uid]["images_extracted"] = self.db[uid].get("images_extracted", 0) + images_count | |
| self.save_db() | |
| def update_setting(self, user_id, key, value): | |
| with self.lock: | |
| self.db = self.load_db() | |
| uid = str(user_id) | |
| if uid in self.db: | |
| self.db[uid]["settings"][key] = value | |
| self.save_db() | |
| db = UserDatabase() | |
| # الحفظ الاحتياطي السحابي (كل ساعة) لضمان عدم ضياع البيانات في Hugging Face | |
| async def backup_task(): | |
| try: | |
| log_channel = bot.get_channel(LOG_CHANNEL_ID) | |
| if log_channel and os.path.exists(USERS_DB_FILE): | |
| async for msg in log_channel.history(limit=10): | |
| if msg.author == bot.user and "💾 النسخة الاحتياطية" in msg.content: | |
| try: await msg.delete() | |
| except: pass | |
| # رفع نسخة من قاعدة البيانات وملف التراكر | |
| files = [discord.File(USERS_DB_FILE)] | |
| if os.path.exists(TRACKER_DB_FILE): files.append(discord.File(TRACKER_DB_FILE)) | |
| await log_channel.send("💾 النسخة الاحتياطية التلقائية لقاعدة البيانات (لا تقم بمسح هذه الرسالة)", files=files) | |
| except Exception as e: | |
| print(f"Backup Error: {e}") | |
| async def send_log(guild, embed, files=None): | |
| if not LOG_CHANNEL_ID: return | |
| channel = guild.get_channel(LOG_CHANNEL_ID) | |
| if channel: | |
| try: await channel.send(embed=embed, files=files or []) | |
| except: pass | |
| # ================================ | |
| # 🖼️ 8. Image Toolbox (Smart Stitching & Splitting) | |
| # ================================ | |
| class ImageToolbox: | |
| def find_blank_y(img, start_y, end_y): | |
| # خوارزمية ذكية للبحث عن مساحة بيضاء (فراغ بين الفقاعات) لتفادي القص الخاطئ | |
| try: | |
| gray = img.convert('L') | |
| best_y = start_y | |
| min_variance = float('inf') | |
| for y in range(start_y, end_y, 10): # قفزات 10 بيكسل لتسريع العملية | |
| row = [gray.getpixel((x, y)) for x in range(gray.width)] | |
| variance = max(row) - min(row) | |
| if variance < min_variance: | |
| min_variance = variance | |
| best_y = y | |
| if min_variance < 5: return y # خط أبيض مثالي | |
| return best_y | |
| except: return (start_y + end_y) // 2 | |
| def check_and_split(image_path, is_advance=False): | |
| try: | |
| with Image.open(image_path) as img: | |
| width, height = img.size | |
| # الوضع الاحترافي: يقص بدقة أعلى لمعالجة أوضح | |
| threshold = 4000 if is_advance else 6000 | |
| if height <= threshold: | |
| return [image_path], False, height | |
| mid = height // 2 | |
| scan_range = int(height * 0.15) | |
| # البحث عن نقطة القص الذكية في المنتصف | |
| best_cut_y = ImageToolbox.find_blank_y(img, max(0, mid - scan_range), min(height, mid + scan_range)) | |
| overlap = 20 | |
| top = img.crop((0, 0, width, min(height, best_cut_y + overlap))) | |
| btm = img.crop((0, max(0, best_cut_y - overlap), width, height)) | |
| base, ext = os.path.splitext(image_path) | |
| p1, p2 = f"{base}_p1{ext}", f"{base}_p2{ext}" | |
| top.save(p1); btm.save(p2) | |
| return [p1, p2], True, height | |
| except: return [image_path], False, 0 | |
| def merge_images_vertically(image_paths, output_path, max_height=50000): | |
| try: | |
| imgs = [Image.open(p).convert('RGB') for p in image_paths] | |
| if not imgs: return False | |
| base_w = max(im.width for im in imgs) | |
| parts, curr_imgs, curr_h = [], [], 0 | |
| for img in imgs: | |
| # توحيد العرض مع الحفاظ على النسبة والتناسب | |
| if img.width != base_w: | |
| img = img.resize((base_w, int(img.height * (base_w / img.width))), Image.Resampling.LANCZOS) | |
| # نظام الدمج الذكي لتفادي كسر الـ max_height بشكل غبي | |
| if curr_h + img.height > max_height and curr_imgs: | |
| parts.append((curr_imgs, base_w, curr_h)) | |
| curr_imgs, curr_h = [], 0 | |
| curr_imgs.append(img) | |
| curr_h += img.height | |
| if curr_imgs: parts.append((curr_imgs, base_w, curr_h)) | |
| out_files = [] | |
| for idx, (p_imgs, w, h) in enumerate(parts): | |
| canv = Image.new('RGB', (w, h), (255, 255, 255)) | |
| y_off = 0 | |
| for im in p_imgs: | |
| canv.paste(im, (0, y_off)) | |
| y_off += im.height | |
| fname = output_path.replace(".jpg", f"_part{idx+1}.jpg") if len(parts) > 1 else output_path | |
| canv.save(fname, quality=90) | |
| out_files.append(fname) | |
| return out_files | |
| except Exception as e: | |
| print(f"Merge Error: {e}") | |
| return False | |
| # ================================ | |
| # 📥 Universal Downloader (Drive, Gofile, Web & gdown Fallback) | |
| # ================================ | |
| class DownloadManager: | |
| def get_drive_real_name(url): | |
| try: | |
| r = requests.get(url, timeout=10) | |
| soup = BeautifulSoup(r.text, 'html.parser') | |
| title = soup.title.string | |
| if title: return title.replace(" - Google Drive", "").strip() | |
| return "Drive_Content" | |
| except: return "Drive_Content" | |
| def extract_archives(folder): | |
| for root, _, files in os.walk(folder): | |
| for file in files: | |
| fp = os.path.join(root, file) | |
| try: | |
| if file.lower().endswith(".zip"): | |
| with zipfile.ZipFile(fp, 'r') as z: z.extractall(root) | |
| os.remove(fp) | |
| elif file.lower().endswith((".rar", ".cbr", ".cbz")): | |
| patoolib.extract_archive(fp, outdir=root, verbosity=-1) | |
| os.remove(fp) | |
| except: pass | |
| for root, dirs, files in os.walk(folder): | |
| for name in files: | |
| if name.startswith('.') or name.startswith('__MACOSX'): | |
| try: os.remove(os.path.join(root, name)) | |
| except: pass | |
| def scrape_drive_selenium(url, output_folder): | |
| driver = None | |
| try: | |
| chrome_options = Options() | |
| chrome_options.add_argument('--headless=new') | |
| chrome_options.add_argument('--no-sandbox') | |
| chrome_options.add_argument('--disable-dev-shm-usage') | |
| chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") | |
| service = Service(ChromeDriverManager().install()) | |
| driver = webdriver.Chrome(service=service, options=chrome_options) | |
| driver.get(url) | |
| time.sleep(5) | |
| page_title = driver.title.replace(" - Google Drive", "").strip() | |
| if not page_title or page_title == "Google Drive": page_title = "Drive_Folder" | |
| last_height = driver.execute_script("return document.body.scrollHeight") | |
| for _ in range(15): | |
| driver.execute_script("window.scrollBy(0, 1500);") | |
| time.sleep(1) | |
| new_height = driver.execute_script("return document.body.scrollHeight") | |
| if new_height == last_height: break | |
| last_height = new_height | |
| time.sleep(2) | |
| elements = driver.find_elements(By.XPATH, "//*[@data-id]") | |
| file_ids = [] | |
| for el in elements: | |
| fid = el.get_attribute("data-id") | |
| if fid and len(fid) > 20 and fid not in file_ids: | |
| file_ids.append(fid) | |
| if not file_ids: | |
| page_source = driver.page_source | |
| found = re.findall(r'\["([^"]{25,})",', page_source) | |
| file_ids = list(set([fid for fid in found if len(fid) > 28])) | |
| if not file_ids: | |
| driver.quit() | |
| return False, "Error" | |
| saved = False | |
| session = requests.Session() | |
| session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}) | |
| for idx, fid in enumerate(file_ids): | |
| try: | |
| dl_url = f"https://drive.google.com/uc?export=download&id={fid}" | |
| r = session.get(dl_url, stream=True, timeout=15) | |
| if r.status_code == 200 and len(r.content) > 10000: | |
| with open(os.path.join(output_folder, f"drive_img_{idx:03d}.jpg"), 'wb') as f: | |
| f.write(r.content) | |
| saved = True | |
| except: pass | |
| driver.quit() | |
| return saved, page_title | |
| except Exception as e: | |
| if driver: driver.quit() | |
| return False, "Error" | |
| async def scrape_drive(url, output_folder): | |
| real_name = DownloadManager.get_drive_real_name(url) | |
| # 1. المحاولة الأساسية باستخدام المتصفح الخفي (Selenium) | |
| success, name = await asyncio.to_thread(DownloadManager.scrape_drive_selenium, url, output_folder) | |
| # 2. خط الدفاع الأخير (Fallback) باستخدام gdown لو فشل المتصفح | |
| if not success: | |
| try: | |
| print("⚠️ فشل السحب باستخدام Selenium، جاري التحويل إلى خطة الطوارئ (gdown)...") | |
| if "folder" in url or "drive.google.com/drive/folders" in url: | |
| res = await asyncio.to_thread(gdown.download_folder, url, output=output_folder, quiet=False, use_cookies=False) | |
| if res: success = True | |
| else: | |
| res = await asyncio.to_thread(gdown.download, url, output=os.path.join(output_folder, "gdown_fallback.zip"), quiet=False, fuzzy=True) | |
| if res: success = True | |
| if success: name = real_name | |
| except Exception as e: | |
| print(f"❌ فشلت خطة الطوارئ gdown أيضاً: {e}") | |
| return success, name | |
| def scrape_gofile_selenium(url, output_folder): | |
| driver = None | |
| try: | |
| chrome_options = Options() | |
| chrome_options.add_argument('--headless=new') | |
| chrome_options.add_argument('--no-sandbox') | |
| chrome_options.add_argument('--disable-dev-shm-usage') | |
| service = Service(ChromeDriverManager().install()) | |
| driver = webdriver.Chrome(service=service, options=chrome_options) | |
| driver.get(url) | |
| WebDriverWait(driver, 15).until(EC.presence_of_element_located((By.CSS_SELECTOR, ".contentName, .downloadButton"))) | |
| time.sleep(3) | |
| try: page_title = driver.find_element(By.CSS_SELECTOR, ".contentName").text | |
| except: page_title = "Gofile_Content" | |
| links = driver.find_elements(By.TAG_NAME, 'a') | |
| download_urls = [link.get_attribute('href') for link in links if link.get_attribute('href') and 'download' in link.get_attribute('href').lower()] | |
| cookies = driver.get_cookies() | |
| session = requests.Session() | |
| for cookie in cookies: session.cookies.set(cookie['name'], cookie['value']) | |
| saved = False | |
| for idx, d_url in enumerate(list(set(download_urls))): | |
| r = session.get(d_url, stream=True) | |
| if r.status_code == 200: | |
| cd = r.headers.get('content-disposition', '') | |
| fname = re.findall("filename=(.+)", cd) | |
| name = fname[0].strip('"') if fname else f"file_{idx}.zip" | |
| with open(os.path.join(output_folder, name), 'wb') as f: f.write(r.content) | |
| saved = True | |
| driver.quit() | |
| return saved, page_title | |
| except: | |
| if driver: driver.quit() | |
| return False, "Error" | |
| def scrape_web_selenium(url, output_folder): | |
| driver = None | |
| try: | |
| chrome_options = Options() | |
| chrome_options.add_argument('--headless=new') | |
| chrome_options.add_argument('--no-sandbox') | |
| chrome_options.add_argument('--disable-dev-shm-usage') | |
| service = Service(ChromeDriverManager().install()) | |
| driver = webdriver.Chrome(service=service, options=chrome_options) | |
| driver.get(url) | |
| time.sleep(5) | |
| last_h = driver.execute_script("return document.body.scrollHeight") | |
| for _ in range(30): | |
| driver.execute_script("window.scrollBy(0, 800);") | |
| time.sleep(0.5) | |
| new_h = driver.execute_script("return window.pageYOffset + window.innerHeight") | |
| if new_h >= last_h: break | |
| last_h = driver.execute_script("return document.body.scrollHeight") | |
| time.sleep(2) | |
| elems = driver.find_elements(By.TAG_NAME, 'img') | |
| urls = [e.get_attribute('src') or e.get_attribute('data-src') for e in elems] | |
| urls = list(set([u for u in urls if u and u.startswith('http') and not any(x in u.lower() for x in ['logo','ad','banner','icon'])])) | |
| saved_paths = [] | |
| req_headers = {'User-Agent': 'Mozilla/5.0'} | |
| for idx, u in enumerate(urls): | |
| try: | |
| c = requests.get(u, headers=req_headers, timeout=10).content | |
| im = Image.open(io.BytesIO(c)).convert('RGB') | |
| if im.width > 150 and im.height > 150: | |
| path = os.path.join(output_folder, f"{idx:03d}.jpg") | |
| im.save(path) | |
| saved_paths.append(path) | |
| except: pass | |
| title = driver.title.split('|')[0].strip() if driver.title else "Web_Chapter" | |
| title = re.sub(r'[\\/*?:"<>|]', "", title) | |
| driver.quit() | |
| return saved_paths, title | |
| except Exception as e: | |
| if driver: driver.quit() | |
| return [], "Error" | |
| async def download(url, output_folder): | |
| success = False | |
| name = "Downloaded_Content" | |
| if "drive.google.com" in url: | |
| success, name = await DownloadManager.scrape_drive(url, output_folder) | |
| elif "gofile.io" in url: | |
| success, name = await asyncio.to_thread(DownloadManager.scrape_gofile_selenium, url, output_folder) | |
| else: | |
| try: | |
| async with aiohttp.ClientSession() as s: | |
| async with s.get(url, headers={'User-Agent': 'Mozilla/5.0'}) as r: | |
| if r.status == 200: | |
| cd = r.headers.get('Content-Disposition', '') | |
| if 'filename=' in cd: name = re.findall("filename=(.+)", cd)[0].strip('"') | |
| else: name = "file.bin" | |
| with open(os.path.join(output_folder, name), 'wb') as f: f.write(await r.read()) | |
| success = True | |
| except: pass | |
| DownloadManager.extract_archives(output_folder) | |
| has_images = any(f.lower().endswith(('.png','.jpg','.jpeg','.webp')) for r, d, files in os.walk(output_folder) for f in files) | |
| if success and has_images: return True, name | |
| return False, "Error downloading or no images found." | |
| def get_chapters_dict(folder): | |
| chapters = {} | |
| subdirs = [d for d in os.listdir(folder) if os.path.isdir(os.path.join(folder, d))] | |
| if not subdirs: | |
| imgs = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))] | |
| if imgs: chapters["Main"] = sorted(imgs) | |
| else: | |
| for d in sorted(subdirs): | |
| d_path = os.path.join(folder, d) | |
| imgs = [os.path.join(d_path, f) for f in os.listdir(d_path) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))] | |
| if imgs: chapters[d] = sorted(imgs) | |
| return chapters | |
| # ================================ | |
| # 🔗 10. Universal Uploader (Gofile + Catbox Fallback) | |
| # ================================ | |
| class FileUploader: | |
| async def upload(file_path): | |
| # المحاولة الأولى: Gofile | |
| try: | |
| server_res = await asyncio.to_thread(requests.get, "https://api.gofile.io/servers") | |
| server = server_res.json()['data']['servers'][0]['name'] | |
| with open(file_path, 'rb') as f: | |
| upload_res = await asyncio.to_thread(requests.post, f"https://{server}.gofile.io/contents/upload", files={'file': f}) | |
| return upload_res.json()['data']['downloadPage'] | |
| except Exception as e: | |
| print(f"⚠️ فشل الرفع على Gofile، جاري التحويل إلى Catbox... ({e})") | |
| # المحاولة الثانية (Fallback): Catbox (سريع وموثوق جداً) | |
| try: | |
| with open(file_path, 'rb') as f: | |
| res = await asyncio.to_thread(requests.post, "https://catbox.moe/user/api.php", data={"reqtype": "fileupload"}, files={"fileToUpload": f}) | |
| if res.status_code == 200: return res.text | |
| except Exception as e: | |
| print(f"❌ فشل الرفع على جميع السيرفرات: {e}") | |
| return None | |
| # ================================ | |
| # 🕹️ 11. UI Components (OCR specific views) | |
| # ================================ | |
| class CostConfirmView(View): | |
| def __init__(self, user_id, cost, lang, on_confirm_callback, tmp_dir): | |
| super().__init__(timeout=86400) | |
| self.user_id = user_id | |
| self.callback = on_confirm_callback | |
| self.tmp_dir = tmp_dir | |
| self.btn_yes.label = "✅ استكمال (نعم)" if lang == "ar" else "✅ Proceed (Yes)" | |
| self.btn_no.label = "❌ إلغاء (لا)" if lang == "ar" else "❌ Cancel (No)" | |
| async def btn_yes(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| for child in self.children: child.disabled = True | |
| await interaction.response.edit_message(content=f"{interaction.user.mention} ⏳ يتم التجهيز للاستخراج...", embed=None, view=None) | |
| await self.callback(interaction) | |
| self.stop() | |
| async def btn_no(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| for child in self.children: child.disabled = True | |
| shutil.rmtree(self.tmp_dir, ignore_errors=True) | |
| await interaction.response.edit_message(content=f"{interaction.user.mention} ⛔ تم الإلغاء وحذف الملفات المؤقتة.", embed=None, view=None) | |
| self.stop() | |
| async def on_timeout(self): | |
| shutil.rmtree(self.tmp_dir, ignore_errors=True) | |
| class ChapterSelectView(View): | |
| def __init__(self, user_id, chapters_dict, on_select_callback, tmp_dir): | |
| super().__init__(timeout=86400) | |
| self.user_id = user_id | |
| self.chapters_dict = chapters_dict | |
| self.callback = on_select_callback | |
| self.tmp_dir = tmp_dir | |
| options = [] | |
| for name, imgs in list(chapters_dict.items())[:24]: | |
| options.append(discord.SelectOption(label=f"فصل: {name[:50]}", description=f"{len(imgs)} صور", value=name)) | |
| options.append(discord.SelectOption(label="🌟 تحديد الكل", description="سحب كل الفصول", value="ALL")) | |
| self.select = Select(placeholder="اختر الفصول المراد معالجتها...", min_values=1, max_values=len(options), options=options) | |
| self.select.callback = self.select_callback | |
| self.add_item(self.select) | |
| async def select_callback(self, interaction: discord.Interaction): | |
| if interaction.user.id != self.user_id: return | |
| self.select.disabled = True | |
| await interaction.response.edit_message(content="✅ تم الاختيار. جاري الحساب...", view=None) | |
| selected_chaps = {} | |
| if "ALL" in self.select.values: selected_chaps = self.chapters_dict | |
| else: | |
| for val in self.select.values: selected_chaps[val] = self.chapters_dict[val] | |
| await self.callback(interaction, selected_chaps) | |
| self.stop() | |
| async def on_timeout(self): | |
| shutil.rmtree(self.tmp_dir, ignore_errors=True) | |
| # ================================ | |
| # 🧩 12. Merge Command (/merge) | |
| # ================================ | |
| async def merge_cmd(interaction: discord.Interaction, link: str, max_height: int = 50000): | |
| user = db.get_user(interaction.user.id, interaction.user.name) | |
| is_free = False | |
| with db.lock: | |
| if db.db[str(interaction.user.id)].get("free_merges", 0) > 0: is_free = True | |
| elif db.db[str(interaction.user.id)]["points"] < 0.1: | |
| return await interaction.response.send_message("❌ رصيدك لا يكفي (مطلوب 0.1 نقطة لعملية الدمج).", ephemeral=True) | |
| await interaction.response.send_message(embed=discord.Embed(title="⏳ جاري سحب الصور للدمج...", color=0x9b59b6)) | |
| msg = await interaction.original_response() | |
| bot_queue["merge"].append(interaction.user.name) | |
| tmp = os.path.join(TEMP_ROOT, f"merge_{interaction.user.id}_{int(time.time())}") | |
| os.makedirs(tmp, exist_ok=True) | |
| try: | |
| success, source_name = await DownloadManager.download(link, tmp) | |
| if not success: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| return await msg.edit(content="❌ فشل سحب الصور من الرابط.") | |
| chapters_dict = DownloadManager.get_chapters_dict(tmp) | |
| if not chapters_dict: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| return await msg.edit(content="❌ لم يتم العثور على صور صالحة للدمج.") | |
| imgs = list(chapters_dict.values())[0] | |
| if len(imgs) < 2: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| return await msg.edit(content="❌ الرابط يحتوي على أقل من صورتين.") | |
| await msg.edit(embed=discord.Embed(title=f"🛠️ جاري دمج الصور بذكاء (الطول الأقصى: {max_height})...", color=0xf1c40f)) | |
| merged_files = await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, os.path.join(tmp, f"{source_name}_merged.jpg"), max_height=max_height) | |
| if not merged_files: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| return await msg.edit(content="❌ فشلت عملية الدمج.") | |
| await msg.edit(embed=discord.Embed(title="☁️ جاري الرفع للسحابة...", color=0x3498db)) | |
| zip_path = os.path.join(tmp, f"{source_name}_Merged.zip") | |
| with zipfile.ZipFile(zip_path, 'w') as z: | |
| for f in merged_files: z.write(f, os.path.basename(f)) | |
| upload_link = await FileUploader.upload(zip_path) | |
| if not upload_link: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| return await msg.edit(content="❌ فشل الرفع على Gofile والسيرفرات البديلة، تم إلغاء العملية ولم يتم خصم نقاط.") | |
| cost_msg = "" | |
| with db.lock: | |
| if is_free: | |
| db.db[str(interaction.user.id)]["free_merges"] -= 1 | |
| rem_free = db.db[str(interaction.user.id)]["free_merges"] | |
| cost_msg = f"🎁 **مجاناً** (تبقى لك {rem_free} مرات مجانية)" | |
| else: | |
| db.db[str(interaction.user.id)]["points"] = round(db.db[str(interaction.user.id)]["points"] - 0.1, 2) | |
| db.db[str(interaction.user.id)]["points_spent"] = round(db.db[str(interaction.user.id)].get("points_spent", 0) + 0.1, 2) | |
| cost_msg = f"💰 **تم خصم 0.1 نقطة** (رصيدك: {db.db[str(interaction.user.id)]['points']} نقطة)" | |
| db.save_db() | |
| res_embed = discord.Embed(title="✅ تم الدمج والرفع بنجاح!", color=0x2ecc71) | |
| res_embed.add_field(name="الرابط:", value=f"[اضغط هنا لتحميل الصور المدمجة]({upload_link})", inline=False) | |
| res_embed.add_field(name="التكلفة:", value=cost_msg, inline=False) | |
| await msg.edit(content=f"{interaction.user.mention}", embed=res_embed) | |
| except Exception as e: | |
| await msg.edit(content=f"⚠️ حدث خطأ أثناء الدمج: {e}") | |
| finally: | |
| if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| # ================================ | |
| # 🚀 13. Core Extraction Commands (/ocr) | |
| # ================================ | |
| def get_rank_multiplier(user): | |
| rank = user.get("rank", "Normal").lower() | |
| if rank == "vvip": return 0.50 | |
| if rank == "vip": return 0.75 | |
| return 1.0 | |
| def calculate_initial_cost(user, original_img_count): | |
| if original_img_count == 1: cost = 0.1 | |
| elif original_img_count <= 15: cost = 1.0 | |
| else: cost = 1.0 + ((original_img_count - 15) * 0.1) | |
| return round(max(0.01, cost * get_rank_multiplier(user)), 2) | |
| def generate_progress_bar(current, total, length=15): | |
| if total == 0: return f"[{'░' * length}]" | |
| percent = current / total | |
| filled = int(length * percent) | |
| return f"[{'█' * filled}{'░' * (length - filled)}]" | |
| async def ocr_cmd(interaction: discord.Interaction, link: str = None, file: discord.Attachment = None): | |
| if not link and not file: return await interaction.response.send_message("❌ يرجى توفير رابط أو إرفاق ملف.", ephemeral=True) | |
| user = db.get_user(interaction.user.id, interaction.user.name) | |
| if user["points"] < 0.1: | |
| return await interaction.response.send_message("❌ رصيدك غير كافٍ للاستخراج.", ephemeral=True) | |
| await interaction.response.send_message(embed=discord.Embed(title="⏳ المرحلة 1: جاري التحميل...", color=0xf1c40f)) | |
| msg = await interaction.original_response() | |
| bot_queue["ocr"].append(interaction.user.name) | |
| start_time = time.time() | |
| task_id = f"ocr_{interaction.user.id}_{int(start_time)}" | |
| tmp = os.path.join(TEMP_ROOT, task_id) | |
| os.makedirs(tmp, exist_ok=True) | |
| source_name = "Extracted_Content" | |
| try: | |
| dl_start = time.time() | |
| if file: | |
| path = os.path.join(tmp, file.filename) | |
| await file.save(path) | |
| DownloadManager.extract_archives(tmp) | |
| source_name = file.filename.replace(".zip", "").replace(".rar", "").replace(".cbz", "") | |
| else: | |
| success, error_msg = await DownloadManager.download(link, tmp) | |
| if not success: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط.") | |
| source_name = error_msg | |
| dl_time = time.time() - dl_start | |
| chapters_dict = DownloadManager.get_chapters_dict(tmp) | |
| if not chapters_dict: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| return await msg.edit(content="❌ لم يتم العثور على صور صالحة للاستخراج.") | |
| if len(chapters_dict) > 1: | |
| info = f"📦 تم العثور على **{len(chapters_dict)}** فصول. يرجى اختيار الفصول المراد معالجتها:" | |
| async def on_chapter_selected(inter, selected_chapters): | |
| await start_parallel_extraction(inter, msg, user, selected_chapters, tmp, task_id, dl_time, source_name) | |
| view = ChapterSelectView(interaction.user.id, chapters_dict, on_chapter_selected, tmp) | |
| await msg.edit(content=info, embed=None, view=view) | |
| else: | |
| await start_parallel_extraction(interaction, msg, user, chapters_dict, tmp, task_id, dl_time, source_name) | |
| except Exception as e: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| await interaction.followup.send(f"⚠️ خطأ غير متوقع: {e}", ephemeral=True) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| async def start_parallel_extraction(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name): | |
| task = asyncio.create_task(process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name)) | |
| active_tasks[task_id] = task | |
| try: await task | |
| except asyncio.CancelledError: pass | |
| finally: | |
| active_tasks.pop(task_id, None); active_tasks.pop(f"{task_id}_progress", None) | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| async def process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name): | |
| all_imgs = [] | |
| for imgs in chapters_dict.values(): all_imgs.extend(imgs) | |
| is_advance = user["settings"].get("advance_mode", False) | |
| original_img_count = len(all_imgs) | |
| total_cost = calculate_initial_cost(user, original_img_count) | |
| lang = user["settings"].get("language", "ar") | |
| async def execute_ocr(inter=interaction): | |
| total_cost_deducted = 0.0 | |
| try: | |
| if db.get_user(inter.user.id, inter.user.name)["points"] < total_cost: | |
| return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة `{total_cost:.2f}`.", view=None) | |
| db.modify_points(inter.user.id, -total_cost, spent=True) | |
| total_cost_deducted = total_cost | |
| ocr_start = time.time() | |
| total_pages = original_img_count | |
| active_tasks[f"{task_id}_progress"] = 0 | |
| def get_ui_embed(curr, total): | |
| e = discord.Embed(title="🚀 جاري استخراج النصوص...", color=0xf1c40f) | |
| e.add_field(name="الملف/الفصل", value=f"`{source_name}`", inline=False) | |
| percent = int((curr/total)*100) if total > 0 else 0 | |
| e.add_field(name="تقدم الصور", value=f"`{generate_progress_bar(curr, total)}` **{percent}%** ({curr}/{total})", inline=False) | |
| return e | |
| cancel_view = CancelView(inter.user.id, task_id, total_pages) | |
| await msg.edit(content=" ", embed=get_ui_embed(0, total_pages), view=cancel_view) | |
| total_bubbles = 0 | |
| failed_pages = 0 | |
| # تقليل السعة لتفادي ضغط API (Load Balancing) | |
| semaphore = asyncio.Semaphore(4) | |
| async def process_single_image(img_path): | |
| async with semaphore: | |
| # نقل القص إلى خيط منفصل لمنع خنق المعالج (CPU Choke Fix) | |
| parts, _, _ = await asyncio.to_thread(ImageToolbox.check_and_split, img_path, is_advance) | |
| page_texts = [] | |
| for p in parts: | |
| raw = await ai_engine.analyze_image(p, advance_mode=is_advance) | |
| page_texts.append(raw) | |
| active_tasks[f"{task_id}_progress"] += 1 | |
| curr_prog = active_tasks[f"{task_id}_progress"] | |
| try: await msg.edit(embed=get_ui_embed(curr_prog, total_pages), view=cancel_view) | |
| except: pass | |
| return page_texts | |
| for chap_name, imgs in chapters_dict.items(): | |
| tasks = [process_single_image(img_path) for img_path in imgs] | |
| results = await asyncio.gather(*tasks) | |
| res_text = [] | |
| for idx, page_parts_raw in enumerate(results): | |
| page_txt = [] | |
| is_failed = True | |
| for raw in page_parts_raw: | |
| if raw.strip(): | |
| cleaned = clean_text(raw, user["settings"]) | |
| if cleaned: | |
| is_failed = False | |
| valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"] | |
| total_bubbles += len(valid_lines) | |
| page_txt.append("\n".join(cleaned)) | |
| if is_failed: failed_pages += 1 | |
| combined_page = "\n".join([pt for pt in page_txt if pt.strip()]) | |
| res_text.append(f"--- Page {idx+1} ---\n{combined_page}") | |
| full_text = "\n\n".join(res_text) | |
| fmt = user["settings"].get("format", "docx") | |
| final_chap_name = source_name if chap_name == "Main" else chap_name | |
| safe_chap_name = re.sub(r'[\\/*?:"<>|]', "", final_chap_name) | |
| out_p = os.path.join(tmp_dir, f"{safe_chap_name}.{fmt}") | |
| if fmt == "docx": | |
| doc = Document() | |
| for line in full_text.split('\n'): doc.add_paragraph(line) | |
| doc.save(out_p) | |
| else: | |
| with open(out_p, 'w', encoding='utf-8') as f: f.write(full_text) | |
| await inter.channel.send(content=f"📄 تم استخراج فصل: **{final_chap_name}**", file=discord.File(out_p)) | |
| db.add_stats(inter.user.id, total_pages) | |
| extract_time = time.time() - ocr_start | |
| total_time = dl_time + extract_time | |
| new_balance = db.get_user(inter.user.id, inter.user.name)["points"] | |
| stats_msg = f"""✅ **Extraction Complete for {inter.user.mention}!** | |
| 💰 **{total_cost:.2f} points deducted. New balance: {new_balance:.2f}** | |
| 📊 **Stats for `{source_name}`:** | |
| • **Total images:** `{total_pages}` | |
| • **Text extracted:** `{total_bubbles}` | |
| • **Failed extractions:** `{failed_pages}` | |
| ⏱️ **Processing Time:** | |
| • **Download:** `{dl_time:.2f}s` | |
| • **Extraction:** `{extract_time:.2f}s` | |
| • **Total:** `{total_time:.2f}s`""" | |
| await msg.delete() | |
| await inter.channel.send(content=stats_msg, view=PostExtractView()) | |
| except asyncio.CancelledError: | |
| if total_cost_deducted > 0: | |
| db.modify_points(inter.user.id, total_cost_deducted, spent=False) | |
| await inter.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة.") | |
| raise | |
| except Exception as e: | |
| if total_cost_deducted > 0: | |
| db.modify_points(inter.user.id, total_cost_deducted, spent=False) | |
| await inter.channel.send(f"♻️ خطأ غير متوقع. تم استرداد **{total_cost_deducted:.2f}** نقطة.") | |
| await inter.channel.send(f"⚠️ خطأ: {e}") | |
| finally: | |
| shutil.rmtree(tmp_dir, ignore_errors=True) | |
| if total_cost > 1.0 and not is_advance: | |
| embed = discord.Embed(title="⚠️ تأكيد الفاتورة", description="فصل طويل، راجع التكلفة:", color=0xe67e22) | |
| embed.add_field(name="الصور", value=f"`{original_img_count}`", inline=True) | |
| embed.add_field(name="التكلفة", value=f"**`{total_cost:.2f}`** نقطة", inline=True) | |
| await msg.edit(content=" ", embed=embed, view=CostConfirmView(interaction.user.id, total_cost, lang, execute_ocr, tmp_dir)) | |
| else: | |
| await execute_ocr(interaction) | |
| # ================================ | |
| # 🌐 14. Web OCR Command (/ocrweb) | |
| # ================================ | |
| async def ocrweb_cmd(interaction: discord.Interaction, link: str): | |
| user = db.get_user(interaction.user.id, interaction.user.name) | |
| if user["points"] < 0.1: | |
| return await interaction.response.send_message("❌ رصيدك لا يكفي (يجب أن يكون 0.1 على الأقل).", ephemeral=True) | |
| embed = discord.Embed(title="🌐 جاري سحب الصور من الموقع (Web Scraping)...", color=0x9b59b6) | |
| await interaction.response.send_message(embed=embed) | |
| msg = await interaction.original_response() | |
| bot_queue["ocr"].append(interaction.user.name) | |
| tmp = os.path.join(TEMP_ROOT, f"web_{interaction.user.id}_{int(time.time())}") | |
| os.makedirs(tmp, exist_ok=True) | |
| total_cost_deducted = 0.0 | |
| try: | |
| dl_start = time.time() | |
| # استخدام دالة الـ Scraper المحسنة من الجزء الثاني | |
| imgs, name = await asyncio.to_thread(DownloadManager.scrape_web_selenium, link, tmp) | |
| dl_time = time.time() - dl_start | |
| if not imgs: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط أو أن الموقع مدعوم.") | |
| merged_path = os.path.join(tmp, f"{name}_merged.jpg") | |
| await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, merged_path) | |
| cost = calculate_initial_cost(user, len(imgs)) + 0.5 | |
| if user["points"] < cost: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة: `{cost:.2f}` نقطة.") | |
| db.modify_points(interaction.user.id, -cost, spent=True) | |
| total_cost_deducted = cost | |
| await msg.edit(embed=discord.Embed(title=f"🚀 جاري استخراج {len(imgs)} صور من الموقع...", color=0x3498db)) | |
| is_advance = user["settings"].get("advance_mode", False) | |
| semaphore = asyncio.Semaphore(4) | |
| extract_start = time.time() | |
| failed_pages = 0 | |
| total_bubbles = 0 | |
| async def process_web_img(idx, img_path): | |
| async with semaphore: | |
| raw = await ai_engine.analyze_image(img_path, advance_mode=is_advance) | |
| return idx, raw | |
| tasks_list = [process_web_img(i, p) for i, p in enumerate(imgs)] | |
| results = await asyncio.gather(*tasks_list) | |
| # الترتيب حسب الاندكس لضمان عدم اللخبطة | |
| results.sort(key=lambda x: x[0]) | |
| res_text = [] | |
| for idx, raw in results: | |
| if not raw.strip(): | |
| failed_pages += 1 | |
| res_text.append(f"--- Page {idx+1} ---\n[Failed to extract text]") | |
| continue | |
| cleaned = clean_text(raw, user["settings"]) | |
| if not cleaned: failed_pages += 1 | |
| else: | |
| valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"] | |
| total_bubbles += len(valid_lines) | |
| combined_page = "\n".join([pt for pt in cleaned if pt.strip()]) | |
| res_text.append(f"--- Page {idx+1} ---\n{combined_page}") | |
| doc_path = os.path.join(tmp, f"{name}.docx") | |
| doc = Document() | |
| for line in ("\n\n".join(res_text)).split('\n'): doc.add_paragraph(line) | |
| doc.save(doc_path) | |
| db.add_stats(interaction.user.id, len(imgs)) | |
| extract_time = time.time() - extract_start | |
| total_time = dl_time + extract_time | |
| new_balance = db.get_user(interaction.user.id, interaction.user.name)["points"] | |
| stats_msg = f"""✅ **Extraction Complete for {interaction.user.mention}!** | |
| 💰 **{cost:.2f} points deducted. New balance: {new_balance:.2f}** | |
| 📊 **Stats for `{name}`:** | |
| • **Total images:** `{len(imgs)}` | |
| • **Text extracted:** `{total_bubbles}` | |
| • **Failed extractions:** `{failed_pages}` | |
| ⏱️ **Processing Time:** | |
| • **Download:** `{dl_time:.2f}s` | |
| • **Extraction:** `{extract_time:.2f}s` | |
| • **Total:** `{total_time:.2f}s`""" | |
| await msg.delete() | |
| await interaction.channel.send(content=stats_msg, files=[discord.File(doc_path), discord.File(merged_path)], view=PostExtractView()) | |
| except Exception as e: | |
| if total_cost_deducted > 0: | |
| db.modify_points(interaction.user.id, total_cost_deducted, spent=False) | |
| await interaction.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة بسبب فشل العملية.") | |
| await msg.edit(content=f"⚠️ خطأ: {e}", embed=None) | |
| finally: | |
| if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name) | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| # ================================ | |
| # ⚙️ 15. Settings Command & UI | |
| # ================================ | |
| class LegendsMainModal(Modal, title="تخصيص العلامات الأساسية"): | |
| speech = TextInput(label='كلام عادي ("")', placeholder='الافتراضي: ""', required=False, max_length=5) | |
| thought = TextInput(label='تفكير (())', placeholder='الافتراضي: ()', required=False, max_length=5) | |
| scream = TextInput(label='صراخ (%)', placeholder='الافتراضي: %', required=False, max_length=5) | |
| connected = TextInput(label='كلام متصل (//)', placeholder='الافتراضي: //', required=False, max_length=5) | |
| def __init__(self, user_id): | |
| super().__init__() | |
| self.user_id = user_id | |
| async def on_submit(self, interaction: discord.Interaction): | |
| user = db.get_user(self.user_id, interaction.user.name) | |
| c = user["settings"].get("custom_tags", {}) | |
| if self.speech.value: c['""'] = self.speech.value | |
| if self.thought.value: c['()'] = self.thought.value | |
| if self.scream.value: c['%'] = self.scream.value | |
| if self.connected.value: c['//'] = self.connected.value | |
| db.update_setting(self.user_id, "custom_tags", c) | |
| await interaction.response.send_message("✅ تم التحديث بنجاح!", ephemeral=True) | |
| class SettingsView(View): | |
| def __init__(self, user_id, user_data): | |
| super().__init__(timeout=120) | |
| self.user_id = user_id | |
| self.settings = user_data["settings"] | |
| self.update_components() | |
| def update_components(self): | |
| for child in self.children: | |
| if isinstance(child, discord.ui.Button): | |
| if child.custom_id == "blk": | |
| child.style = discord.ButtonStyle.success if self.settings.get("blank_line", True) else discord.ButtonStyle.secondary | |
| elif child.custom_id == "sfx": | |
| child.style = discord.ButtonStyle.success if self.settings.get("include_sfx", False) else discord.ButtonStyle.secondary | |
| elif child.custom_id == "adv": | |
| child.style = discord.ButtonStyle.success if self.settings.get("advance_mode", False) else discord.ButtonStyle.secondary | |
| async def legend_select(self, interaction: discord.Interaction, select: Select): | |
| if interaction.user.id != self.user_id: return | |
| await interaction.response.send_modal(LegendsMainModal(self.user_id)) | |
| async def blank_btn(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| val = not self.settings.get("blank_line", True) | |
| db.update_setting(self.user_id, "blank_line", val) | |
| self.settings["blank_line"] = val | |
| self.update_components() | |
| await interaction.response.edit_message(view=self) | |
| async def sfx_btn(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| val = not self.settings.get("include_sfx", False) | |
| db.update_setting(self.user_id, "include_sfx", val) | |
| self.settings["include_sfx"] = val | |
| self.update_components() | |
| await interaction.response.edit_message(view=self) | |
| async def adv_btn(self, interaction: discord.Interaction, button: Button): | |
| if interaction.user.id != self.user_id: return | |
| val = not self.settings.get("advance_mode", False) | |
| db.update_setting(self.user_id, "advance_mode", val) | |
| self.settings["advance_mode"] = val | |
| self.update_components() | |
| await interaction.response.edit_message(view=self) | |
| async def settings_cmd(interaction: discord.Interaction): | |
| user = db.get_user(interaction.user.id, interaction.user.name) | |
| embed = discord.Embed( | |
| title="⚙️ لوحة الإعدادات (Settings)", | |
| description="قم بتخصيص طريقة الاستخراج الخاصة بك:\n\n🟢 **أخضر** = مُفعل | ⚪ **رمادي** = مُعطل\n\n⚡ **Advance Mode:** الوضع الاحترافي لاستخراج فائق الدقة.", | |
| color=0x3498db | |
| ) | |
| if interaction.user.avatar: embed.set_thumbnail(url=interaction.user.avatar.url) | |
| await interaction.response.send_message(embed=embed, view=SettingsView(interaction.user.id, user), ephemeral=True) | |
| # ================================ | |
| # 🎫 19. Ticket & Support System | |
| # ================================ | |
| class TicketControlView(discord.ui.View): | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def close(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| await interaction.response.send_message("⏳ جاري إغلاق التذكرة وحفظ السجل...", ephemeral=True) | |
| transcript = f"--- سجل تذكرة {interaction.channel.name} ---\n\n" | |
| async for msg in interaction.channel.history(limit=100, oldest_first=True): | |
| transcript += f"[{msg.created_at.strftime('%Y-%m-%d %H:%M')}] {msg.author.name}: {msg.content}\n" | |
| log_file = discord.File(io.StringIO(transcript), filename=f"{interaction.channel.name}_log.txt") | |
| log_channel = bot.get_channel(LOG_CHANNEL_ID) | |
| if log_channel: | |
| await log_channel.send(content=f"📁 سجل تذكرة مغلقة: {interaction.channel.name}", file=log_file) | |
| await asyncio.sleep(2) | |
| await interaction.channel.delete(reason="تم إغلاق التذكرة.") | |
| class TicketSetupView(discord.ui.View): | |
| def __init__(self): | |
| super().__init__(timeout=None) | |
| async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button): | |
| guild = interaction.guild | |
| category = discord.utils.get(guild.categories, name="🎫 • Tickets") | |
| if not category: | |
| category = await guild.create_category("🎫 • Tickets") | |
| overwrites = { | |
| guild.default_role: discord.PermissionOverwrite(read_messages=False), | |
| interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True, attach_files=True), | |
| guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_channels=True) | |
| } | |
| ticket_channel = await guild.create_text_channel(f"تذكرة-{interaction.user.name}", category=category, overwrites=overwrites) | |
| embed = discord.Embed(title="🎫 تذكرة دعم فني", description=f"أهلاً بك {interaction.user.mention}!\nيرجى طرح مشكلتك أو طلبك بالتفصيل، وسيقوم أحد المشرفين بالرد عليك قريباً.\n\nلإغلاق التذكرة، اضغط على الزر بالأسفل.", color=0x3498db) | |
| await ticket_channel.send(content=f"{interaction.user.mention}", embed=embed, view=TicketControlView()) | |
| await interaction.response.send_message(f"✅ تم فتح تذكرتك بنجاح: {ticket_channel.mention}", ephemeral=True) | |
| async def setup_tickets_cmd(interaction: discord.Interaction): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True) | |
| embed = discord.Embed(title="🛠️ مركز الدعم الفني والمساعدة", description="إذا واجهتك أي مشكلة في الاستخراج، أو ترغب في الانضمام للفريق، أو لديك استفسار للإدارة...\n\n**اضغط على الزر بالأسفل لفتح تذكرة خاصة بك! ⬇️**", color=0x2c3e50) | |
| await interaction.channel.send(embed=embed, view=TicketSetupView()) | |
| await interaction.response.send_message("✅ تم إنشاء لوحة التذاكر.", ephemeral=True) | |
| # ================================ | |
| # 🛡️ 20. Auto-Mod, Trivia & Engagement | |
| # ================================ | |
| active_trivia = {} | |
| MANHWA_DB = [ | |
| {"name": "سونغ جين وو", "hint": "بطل سولو ليفلينج (Solo Leveling)", "img": "https://i.pinimg.com/736x/1a/0a/7b/1a0a7b4f59fb59173f478f2674b967a5.jpg"}, | |
| {"name": "كيم دوكجا", "hint": "بطل وجهة نظر القارئ الكلي العلم (ORV)", "img": "https://i.pinimg.com/736x/d6/3e/26/d63e261cefc48b26e0bdf605e55b6ef1.jpg"}, | |
| {"name": "آرثر", "hint": "بطل البداية بعد النهاية (TBATE)", "img": "https://i.pinimg.com/736x/8e/31/76/8e3176d65b169b2d837697ab41ec85c7.jpg"}, | |
| {"name": "تشونغ ميونغ", "hint": "بطل عودة طائفة جبل هوا", "img": "https://i.pinimg.com/736x/6f/a7/9d/6fa79da2295ed749d214a1e9c8bc8059.jpg"} | |
| ] | |
| async def on_message(message: discord.Message): | |
| if message.author.bot: return | |
| guild_id = message.guild.id | |
| if guild_id in active_trivia: | |
| game = active_trivia[guild_id] | |
| if message.channel.id == game["channel_id"]: | |
| if game["answer"].lower() in message.content.lower(): | |
| winner = message.author | |
| points = game["points"] | |
| db.modify_points(winner.id, points) | |
| embed = discord.Embed(title="🎉 لدينا فائز!", description=f"ألف مبروك لـ {winner.mention} عبقري المانهوا!\nالإجابة الصحيحة هي: **{game['answer']}**\n\n🎁 **كسبت {points:.2f} نقطة استخراج!**", color=0x2ecc71) | |
| await message.channel.send(embed=embed) | |
| del active_trivia[guild_id] | |
| if not message.author.guild_permissions.manage_messages: | |
| if "discord.gg/" in message.content or "discord.com/invite/" in message.content: | |
| await message.delete() | |
| try: | |
| await message.author.timeout(timedelta(minutes=10), reason="إرسال رابط سيرفر خارجي") | |
| warn_msg = await message.channel.send(f"⚠️ {message.author.mention} تم إعطاؤك Mute لمدة 10 دقائق بسبب نشر روابط خارجية!") | |
| await asyncio.sleep(5); await warn_msg.delete() | |
| except: pass | |
| async def trivia_auto_cmd(interaction: discord.Interaction): | |
| if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True) | |
| guild_id = interaction.guild.id | |
| if guild_id in active_trivia: return await interaction.response.send_message("❌ هناك لعبة نشطة بالفعل!", ephemeral=True) | |
| character = random.choice(MANHWA_DB) | |
| points_reward = round(random.uniform(0.1, 1.5), 2) | |
| active_trivia[guild_id] = {"answer": character["name"], "points": points_reward, "channel_id": interaction.channel.id} | |
| embed = discord.Embed(title="🎮 لُعْبَة خَمِّن الْمَانْهْوَا!", description=f"أول شخص يكتب اسم هذه الشخصية بشكل صحيح سيفوز بـ **{points_reward} نقطة استخراج!** 🎁", color=0xe67e22) | |
| embed.add_field(name="💡 تلميح:", value=character["hint"], inline=False) | |
| embed.set_image(url=character["img"]) | |
| await interaction.response.send_message(embed=embed) | |
| async def embed_cmd(interaction: discord.Interaction, title: str, description: str, color: str = "blue", image_url: str = None): | |
| if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True) | |
| color_mapping = {"red": 0xe74c3c, "blue": 0x3498db, "green": 0x2ecc71, "yellow": 0xf1c40f, "black": 0x000000} | |
| embed = discord.Embed(title=title, description=description.replace("\\n", "\n"), color=color_mapping.get(color.lower(), 0x3498db)) | |
| if image_url: embed.set_image(url=image_url) | |
| await interaction.channel.send(embed=embed) | |
| await interaction.response.send_message("✅ تم إرسال الإعلان.", ephemeral=True) | |
| async def giveaway_start_cmd(interaction: discord.Interaction, prize: str, hours: int, winners_count: int = 1): | |
| if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True) | |
| end_time = discord.utils.utcnow() + timedelta(hours=hours) | |
| embed = discord.Embed(title="🎉 سحب جديد! (Giveaway)", description=f"**الجائزة:** {prize}\n**عدد الفائزين:** {winners_count}\n\nاضغط على الريأكشن 🎉 للمشاركة!\n\n⏳ **ينتهي في:** <t:{int(end_time.timestamp())}:R>", color=0x9b59b6) | |
| await interaction.response.send_message("✅ تم إطلاق السحب!", ephemeral=True) | |
| msg = await interaction.channel.send(embed=embed) | |
| await msg.add_reaction("🎉") | |
| async def finish_giveaway(): | |
| await asyncio.sleep(hours * 3600) | |
| new_msg = await interaction.channel.fetch_message(msg.id) | |
| reaction = discord.utils.get(new_msg.reactions, emoji="🎉") | |
| users = [user async for user in reaction.users() if not user.bot] | |
| if len(users) == 0: return await interaction.channel.send(f"❌ انتهى السحب ولم يشارك أحد!") | |
| winners = random.sample(users, min(winners_count, len(users))) | |
| winners_mentions = ", ".join([w.mention for w in winners]) | |
| win_embed = discord.Embed(title="🎊 انتهى السحب!", description=f"**الجائزة:** {prize}\n**الفائزون:** {winners_mentions}", color=0x2ecc71) | |
| await new_msg.reply(content=f"مبروك لـ {winners_mentions}!", embed=win_embed) | |
| bot.loop.create_task(finish_giveaway()) | |
| async def on_raw_reaction_add(payload: discord.RawReactionActionEvent): | |
| if payload.emoji.name == "⭐": | |
| channel = bot.get_channel(payload.channel_id) | |
| msg = await channel.fetch_message(payload.message_id) | |
| reaction = discord.utils.get(msg.reactions, emoji="⭐") | |
| if reaction and reaction.count >= 5: | |
| starboard_channel = discord.utils.get(msg.guild.channels, name="أفضل-اللقطات") | |
| if not starboard_channel: return | |
| async for old_msg in starboard_channel.history(limit=50): | |
| if str(msg.id) in old_msg.content: return | |
| embed = discord.Embed(description=msg.content, color=0xf1c40f, timestamp=msg.created_at) | |
| embed.set_author(name=msg.author.display_name, icon_url=msg.author.avatar.url if msg.author.avatar else None) | |
| if msg.attachments: embed.set_image(url=msg.attachments[0].url) | |
| await starboard_channel.send(content=f"⭐ **{reaction.count}** | {channel.mention} | ID: {msg.id}", embed=embed) | |
| async def on_member_join(member): | |
| guild = member.guild | |
| try: | |
| role = guild.get_role(AUTO_ROLE_ID) | |
| if role: await member.add_roles(role) | |
| except: pass | |
| welcome_channel = guild.get_channel(WELCOME_CHANNEL_ID) | |
| if welcome_channel: | |
| embed = discord.Embed(title=f"مرحباً بك في {guild.name}! 🎭⚡", description=f"أهلاً بك {member.mention}!\n\n📊 **أنت العضو رقم:** `{guild.member_count}`\n🎁 اكتب `/register` للحصول على غرفتك و 3 نقاط مجانية!", color=0x2ecc71) | |
| if member.avatar: embed.set_thumbnail(url=member.avatar.url) | |
| await welcome_channel.send(content=member.mention, embed=embed) | |
| async def live_stats_updater(): | |
| for guild in bot.guilds: | |
| for channel in guild.voice_channels: | |
| if "الأعضاء:" in channel.name or "Members:" in channel.name: | |
| try: await channel.edit(name=f"👥 الأعضاء: {guild.member_count}") | |
| except: pass | |
| def get_emoji_for_name(name): | |
| name = name.lower() | |
| if any(k in name for k in ["ترحيب", "welcome"]): return "👋" | |
| if any(k in name for k in ["شات", "سوالف", "chat", "عام"]): return "💬" | |
| if any(k in name for k in ["إدارة", "admin", "مشرفين"]): return "🛡️" | |
| if any(k in name for k in ["تذاكر", "ticket"]): return "🎫" | |
| if any(k in name for k in ["قوانين", "rules"]): return "📜" | |
| return "📁" | |
| async def setup_server_cmd(interaction: discord.Interaction): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| guild = interaction.guild | |
| if not discord.utils.get(guild.channels, name="أفضل-اللقطات"): | |
| await guild.create_text_channel("أفضل-اللقطات", topic="تحصل الرسائل المميزة على ⭐ لتظهر هنا!") | |
| for category in guild.categories: | |
| if not re.match(r'^[^\w\s]\s*•', category.name): | |
| try: await category.edit(name=f"{get_emoji_for_name(category.name)} • {category.name}") | |
| except: pass | |
| for channel in guild.text_channels: | |
| if not re.match(r'^[^\w\s]ـ', channel.name) and "-" not in channel.name[:3]: | |
| try: await channel.edit(name=f"{get_emoji_for_name(channel.name)}ـ{channel.name.replace(' ', '-')}") | |
| except: pass | |
| await interaction.followup.send("✅ تم تهيئة وتزيين السيرفر بنجاح!") | |
| # ================================ | |
| # 💰 21. Admin, Economy & Core Commands | |
| # ================================ | |
| async def add_cr(interaction: discord.Interaction, user: discord.User, amount: float): | |
| if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True) | |
| new_points = db.modify_points(user.id, amount) | |
| await interaction.response.send_message(f"✅ تمت إضافة **{amount}** لـ {user.mention}. الرصيد: **{new_points:.2f}**") | |
| async def rem_cr(interaction: discord.Interaction, user: discord.User, amount: float): | |
| if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True) | |
| new_points = db.modify_points(user.id, -amount) | |
| await interaction.response.send_message(f"✅ تم خصم **{amount}** من {user.mention}. الرصيد: **{new_points:.2f}**") | |
| async def add_rank(interaction: discord.Interaction, user: discord.User, rank: app_commands.Choice[str]): | |
| if not interaction.user.guild_permissions.administrator and not db.is_bot_admin(str(interaction.user.id)): return await interaction.response.send_message("❌", ephemeral=True) | |
| with db.lock: | |
| uid = str(user.id) | |
| if uid in db.db: | |
| db.db[uid]["rank"] = rank.value | |
| db.save_db() | |
| await interaction.response.send_message(f"✅ تم تغيير رتبة {user.mention} إلى **{rank.value}**") | |
| async def add_admin_cmd(interaction: discord.Interaction, user: discord.User): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True) | |
| db.add_bot_admin(str(user.id)) | |
| await interaction.response.send_message(f"✅ تم إضافة {user.mention} كأدمن للبوت بنجاح!") | |
| async def cleanup_rooms_cmd(interaction: discord.Interaction, days_inactive: int = 30): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True) | |
| await interaction.response.send_message("⏳ جاري التنظيف...", ephemeral=True) | |
| category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON") | |
| if not category: return await interaction.followup.send("❌ القسم غير موجود.") | |
| deleted, cutoff = 0, discord.utils.utcnow() - timedelta(days=days_inactive) | |
| for channel in category.channels: | |
| if isinstance(channel, discord.TextChannel): | |
| last_msg = None | |
| async for msg in channel.history(limit=1): last_msg = msg | |
| if last_msg is None or last_msg.created_at < cutoff: | |
| await channel.delete(reason="تنظيف تلقائي") | |
| deleted += 1 | |
| await asyncio.sleep(1) | |
| await interaction.followup.send(f"🧹 تم حذف **{deleted}** غرفة ميتة.") | |
| async def broadcast_cmd(interaction: discord.Interaction, message: str): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True) | |
| await interaction.response.send_message("⏳ جاري الإرسال...", ephemeral=True) | |
| category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON") | |
| count, embed = 0, discord.Embed(title="📢 إعلان إداري", description=message, color=0xe74c3c) | |
| for channel in category.channels: | |
| try: | |
| await channel.send(embed=embed) | |
| count += 1 | |
| await asyncio.sleep(0.5) | |
| except: pass | |
| await interaction.followup.send(f"✅ تم الإرسال إلى {count} غرفة.") | |
| async def lock_room(interaction: discord.Interaction): | |
| if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True) | |
| ow = interaction.channel.overwrites_for(interaction.guild.default_role) | |
| ow.send_messages = False | |
| await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow) | |
| await interaction.response.send_message("🔒 تم قفل الغرفة!") | |
| async def unlock_room(interaction: discord.Interaction): | |
| if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True) | |
| ow = interaction.channel.overwrites_for(interaction.guild.default_role) | |
| ow.send_messages = True | |
| await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow) | |
| await interaction.response.send_message("🔓 تم فتح الغرفة!") | |
| MODELS_LIST = [app_commands.Choice(name=m, value=m) for m in ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-3.0-pro", "gemini-2.0-flash"]] | |
| async def set_models_cmd(interaction: discord.Interaction, normal_model: app_commands.Choice[str], advance_model: app_commands.Choice[str]): | |
| if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True) | |
| db.update_bot_setting("normal_model", normal_model.value) | |
| db.update_bot_setting("advance_model", advance_model.value) | |
| await interaction.response.send_message("✅ تم تحديث الموديلات.", ephemeral=True) | |
| async def add_emoji_cmd(interaction: discord.Interaction, name: str, link: str = None, image: discord.Attachment = None): | |
| if not interaction.user.guild_permissions.manage_emojis: return await interaction.response.send_message("❌", ephemeral=True) | |
| await interaction.response.defer(ephemeral=True) | |
| img_data = await image.read() if image else requests.get(link).content if link else None | |
| if img_data: | |
| emoji = await interaction.guild.create_custom_emoji(name=name, image=img_data) | |
| await interaction.followup.send(f"✅ تم: {emoji}") | |
| async def status_cmd(interaction: discord.Interaction): | |
| total = len(bot_queue["ocr"]) + len(bot_queue["merge"]) | |
| embed = discord.Embed(title="🚦 حالة البوت", description=f"يوجد **{total}** عمليات قيد التنفيذ." if total else "🟢 البوت خامل حالياً.", color=0x3498db) | |
| await interaction.response.send_message(embed=embed) | |
| async def profile_cmd(interaction: discord.Interaction, target_user: discord.User = None): | |
| t = target_user or interaction.user | |
| d = db.get_user(t.id, t.name) | |
| embed = discord.Embed(title=f"👤 الملف الشخصي: {t.name}", color=0xf1c40f) | |
| embed.add_field(name="💰 الرصيد", value=f"**{d['points']:.2f}**", inline=True) | |
| embed.add_field(name="🏆 الرتبة", value=f"**{d.get('rank','Normal')}**", inline=True) | |
| embed.add_field(name="🖼️ صور مستخرجة", value=f"**{d.get('images_extracted', 0)}**", inline=True) | |
| if t.avatar: embed.set_thumbnail(url=t.avatar.url) | |
| await interaction.response.send_message(embed=embed) | |
| async def top_cmd(interaction: discord.Interaction): | |
| with db.lock: users = sorted([u for u in db.load_db().values() if isinstance(u, dict)], key=lambda x: x.get("images_extracted", 0), reverse=True)[:5] | |
| embed = discord.Embed(title="🏆 لوحة الشرف (Top 5)", color=0xf1c40f) | |
| for i, u in enumerate(users): embed.add_field(name=f"{['🥇','🥈','🥉','🏅','🏅'][i]} {u.get('name','مجهول')}", value=f"استخرج: {u.get('images_extracted',0)}", inline=False) | |
| await interaction.response.send_message(embed=embed) | |
| async def register_cmd(interaction: discord.Interaction): | |
| user = db.get_user(interaction.user.id, interaction.user.name) | |
| if user.get("registered"): return await interaction.response.send_message("❌ أنت مسجل بالفعل!", ephemeral=True) | |
| db.modify_points(interaction.user.id, 3.0) | |
| with db.lock: | |
| db.db[str(interaction.user.id)]["registered"] = True | |
| db.save_db() | |
| guild = interaction.guild | |
| category = discord.utils.get(guild.categories, name="🎭 • MA SON") or await guild.create_category("🎭 • MA SON") | |
| overwrites = {guild.default_role: discord.PermissionOverwrite(read_messages=False), interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True)} | |
| ch = await guild.create_text_channel(f"مكتب-{interaction.user.name}", category=category, overwrites=overwrites) | |
| await ch.send(f"مرحباً {interaction.user.mention} 🎁 لقد حصلت على 3 نقاط مجانية!") | |
| await interaction.response.send_message(f"✅ تم إنشاء غرفتك: {ch.mention}", ephemeral=True) | |
| async def info_cmd(interaction: discord.Interaction): | |
| embed = discord.Embed(title="ℹ️ معلومات البوت", color=0x2c3e50) | |
| embed.add_field(name="👑 المالك", value=interaction.guild.owner.mention, inline=True) | |
| if interaction.user.guild_permissions.administrator: | |
| embed.add_field(name="🛠️ أوامر الإدارة", value="`/setup_server` `/setup_tickets` `/giveaway_start` `/embed` `/trivia_auto`\n`/add_credits` `/remove_credits` `/add_admin` `/cleanup_rooms` `/broadcast`", inline=False) | |
| await interaction.response.send_message(embed=embed, ephemeral=True) | |
| async def help_cmd(interaction: discord.Interaction): | |
| embed = discord.Embed(title="📚 دليل أوامر البوت", color=0x2ecc71) | |
| embed.add_field(name="☁️ الاستخراج والدمج", value="**`/ocr`**: استخراج النصوص.\n**`/ocrweb`**: استخراج من الروابط.\n**`/merge`**: دمج صور الفصول.\n**`/settings`**: لوحة تحكم الاستخراج.", inline=False) | |
| embed.add_field(name="🌍 الترجمة والمساعدة", value="**`/translate`**: ترجمة المانهوا.\n**`/rewrite`**: إعادة صياغة بأسلوب درامي أو كوميدي.", inline=False) | |
| embed.add_field(name="👤 حسابك", value="**`/register`**: إنشاء الغرفة واستلام الهدية.\n**`/profile`**: عرض رصيدك.\n**`/top`**: أفضل المترجمين.", inline=False) | |
| await interaction.response.send_message(embed=embed, ephemeral=True) | |
| # ================================ | |
| # 🚀 22. Bot Startup & Hugging Face Keep-Alive | |
| # ================================ | |
| async def advanced_tracker_loop(): | |
| tracker_data = load_tracker() | |
| scraper = cloudscraper.create_scraper() | |
| for channel_id_str, sites in tracker_data.get("channels", {}).items(): | |
| channel = bot.get_channel(int(channel_id_str)) | |
| if not channel: continue | |
| for site in sites: | |
| try: | |
| res = await asyncio.to_thread(scraper.get, site["url"], timeout=10) | |
| soup = BeautifulSoup(res.text, 'html.parser') | |
| links = soup.find_all('a', href=True) | |
| chapter_links = [a['href'] for a in links if re.search(r'chapter|ch|فصل|episode', a['href'].lower())] | |
| current_hash = chapter_links[0] if chapter_links else str(len(res.text)) | |
| if current_hash != site.get("latest_hash"): | |
| site["latest_hash"] = current_hash | |
| save_tracker(tracker_data) | |
| new_url = current_hash if current_hash.startswith("http") else urllib.parse.urljoin(site["url"], current_hash) | |
| embed = discord.Embed( | |
| title=f"🚀 تحديث جديد: {site['title_ar']}", | |
| description=f"تم صدور فصل جديد من المانهوا!\n\n🔗 **[اضغط هنا لقراءة الفصل]({new_url})**", | |
| color=0xe74c3c | |
| ) | |
| embed.set_thumbnail(url=site["img_url"]) | |
| embed.set_footer(text=f"العمل الأصلي: {site['title_en']}") | |
| await channel.send(embed=embed) | |
| await asyncio.sleep(2) | |
| except Exception as e: | |
| pass | |
| async def on_ready(): | |
| print("⏳ جاري البحث عن نسخ احتياطية للبيانات في ديسكورد...") | |
| try: | |
| log_channel = bot.get_channel(LOG_CHANNEL_ID) | |
| if log_channel: | |
| async for msg in log_channel.history(limit=50): | |
| if msg.author == bot.user and msg.attachments: | |
| found_files = 0 | |
| for att in msg.attachments: | |
| if att.filename == "users_db.json": | |
| await att.save(USERS_DB_FILE) | |
| db.db = db.load_db() | |
| found_files += 1 | |
| elif att.filename == "tracker_cache.json": | |
| await att.save(TRACKER_DB_FILE) | |
| found_files += 1 | |
| if found_files > 0: | |
| print("✅ تم استرجاع البيانات بنجاح من ديسكورد!") | |
| break | |
| except Exception as e: | |
| print(f"⚠️ خطأ في الاسترجاع: {e}") | |
| if not backup_task.is_running(): backup_task.start() | |
| if not live_stats_updater.is_running(): live_stats_updater.start() | |
| if not advanced_tracker_loop.is_running(): advanced_tracker_loop.start() | |
| try: | |
| await bot.tree.sync() | |
| print("🔄 Synced Commands Successfully") | |
| except Exception as e: print(f"❌ Failed to sync: {e}") | |
| print(f'🔥 Logged in as {bot.user} (ID: {bot.user.id})') | |
| await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="Over Webtoons | /help")) | |
| # تشغيل الخادم الوهمي الخاص بـ Hugging Face لضمان بقاء البوت أونلاين 24/7 | |
| keep_alive() | |
| if __name__ == "__main__": | |
| bot.run(DISCORD_TOKEN) | |