Yous135774 commited on
Commit
f74147c
·
verified ·
1 Parent(s): b4acad5

Create bot.py

Browse files
Files changed (1) hide show
  1. bot.py +1766 -0
bot.py ADDED
@@ -0,0 +1,1766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import subprocess
3
+ import os
4
+ import importlib
5
+ import asyncio
6
+ import re
7
+ import time
8
+ import shutil
9
+ import json
10
+ import zipfile
11
+ import urllib.parse
12
+ import random
13
+ import io
14
+ import threading
15
+ from datetime import datetime, timedelta
16
+
17
+ # ================================
18
+ # 🛠️ 1. Auto-Installer & Dependencies
19
+ # ================================
20
+ def install_dependencies():
21
+ packages = [
22
+ ("discord.py", "discord"),
23
+ ("google-generativeai", "google.generativeai"),
24
+ ("aiohttp", "aiohttp"),
25
+ ("nest_asyncio", "nest_asyncio"),
26
+ ("python-docx", "docx"),
27
+ ("patool", "patoolib"),
28
+ ("Pillow", "PIL"),
29
+ ("bs4", "bs4"),
30
+ ("requests", "requests"),
31
+ ("cloudscraper", "cloudscraper"),
32
+ ("selenium", "selenium"),
33
+ ("webdriver-manager", "webdriver_manager"),
34
+ ("gdown", "gdown"),
35
+ ("flask", "flask")
36
+ ]
37
+ try: subprocess.run(["unrar"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
38
+ except: pass
39
+
40
+ try:
41
+ 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)
42
+ except: pass
43
+
44
+ for pkg, import_name in packages:
45
+ try: importlib.import_module(import_name)
46
+ except ImportError:
47
+ try: subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "--disable-pip-version-check", "-U", pkg])
48
+ except: pass
49
+
50
+ install_dependencies()
51
+
52
+ import discord
53
+ from discord.ext import commands, tasks
54
+ from discord import app_commands
55
+ from discord.ui import Select, View, Button, Modal, TextInput
56
+ import google.generativeai as genai
57
+ import aiohttp
58
+ import nest_asyncio
59
+ from docx import Document
60
+ import patoolib
61
+ from PIL import Image, ImageDraw, ImageFont, ImageFile
62
+ from bs4 import BeautifulSoup
63
+ import requests
64
+ import cloudscraper
65
+ import gdown
66
+ from flask import Flask
67
+ from threading import Thread
68
+
69
+ from selenium import webdriver
70
+ from selenium.webdriver.chrome.service import Service
71
+ from selenium.webdriver.chrome.options import Options
72
+ from selenium.webdriver.common.by import By
73
+ from selenium.webdriver.support.ui import WebDriverWait
74
+ from selenium.webdriver.support import expected_conditions as EC
75
+ from webdriver_manager.chrome import ChromeDriverManager
76
+
77
+ nest_asyncio.apply()
78
+ Image.MAX_IMAGE_PIXELS = None
79
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
80
+
81
+ # ================================
82
+ # 🌐 2. Keep-Alive Server (Hugging Face)
83
+ # ================================
84
+ app = Flask('')
85
+ @app.route('/')
86
+ def home():
87
+ return "Bot is running perfectly!"
88
+
89
+ def run_server():
90
+ app.run(host='0.0.0.0', port=7860)
91
+
92
+ def keep_alive():
93
+ t = Thread(target=run_server)
94
+ t.start()
95
+
96
+ # ================================
97
+ # ⚙️ 3. Config & Keys
98
+ # ================================
99
+ # سيتم جلب التوكن والمفاتيح من الـ Secrets في Hugging Face للحماية
100
+ DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
101
+ RAW_KEYS = os.getenv("GEMINI_KEYS", "")
102
+ GEMINI_KEYS = [k.strip() for k in RAW_KEYS.split(",") if k.strip()]
103
+
104
+ WELCOME_CHANNEL_ID = 123456789012345678
105
+ AUTO_ROLE_ID = 123456789012345678
106
+ LOG_CHANNEL_ID = 123456789012345678
107
+ TRACKER_CHANNEL_ID = 123456789012345678
108
+
109
+ BASE_DIR = os.getcwd()
110
+ TEMP_ROOT = os.path.join(BASE_DIR, "bot_temp")
111
+ USERS_DB_FILE = os.path.join(BASE_DIR, "users_db.json")
112
+ TRACKER_DB_FILE = os.path.join(BASE_DIR, "tracker_cache.json")
113
+ active_tasks = {}
114
+ invites_cache = {}
115
+ bot_queue = {"ocr": [], "merge": []}
116
+
117
+ intents = discord.Intents.all()
118
+ bot = commands.Bot(command_prefix="!", intents=intents)
119
+
120
+ def natural_sort_key(s):
121
+ return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)]
122
+
123
+ # ================================
124
+ # 🧠 4. Gemini Smart Manager (The Core Engine)
125
+ # ================================
126
+ class GeminiManager:
127
+ def __init__(self):
128
+ self.keys = GEMINI_KEYS
129
+ self.cooldowns = {key: 0 for key in self.keys}
130
+ self.lock = threading.Lock()
131
+
132
+ def get_valid_key(self):
133
+ with self.lock:
134
+ current_time = time.time()
135
+ available_keys = [k for k, cooldown in self.cooldowns.items() if current_time > cooldown]
136
+ if not available_keys:
137
+ return None
138
+ return random.choice(available_keys)
139
+
140
+ def report_dead_key(self, key):
141
+ with self.lock:
142
+ self.cooldowns[key] = time.time() + 7200
143
+ print(f"🔴 مفتاح تم حظره لمدة ساعتين: {key[:10]}...")
144
+
145
+ async def analyze_image(self, image_path, advance_mode=False):
146
+ prompt = """TASK: STRICT OCR ONLY (VISION + COMPLETENESS ENFORCED)
147
+
148
+ ==================================================
149
+ CORE PRINCIPLES (NON-NEGOTIABLE)
150
+ ==================================================
151
+ - VISUAL SHAPE ONLY.
152
+ - NO inference, NO guessing, NO hallucination.
153
+ - Language, meaning, emotion, size, and layout are IRRELEVANT.
154
+ - Extract ONLY what is visibly present on the page.
155
+ - COMPLETE the current page BEFORE moving to the next.
156
+
157
+ ==================================================
158
+ ORDER NO. 1: ABSOLUTE EXTRACTION RULES
159
+ ==================================================
160
+ 1. NO TRANSLATION: Output the EXACT original text only.
161
+ 2. NO HALLUCINATION: DO NOT invent, guess, infer, or autocomplete text. If text is NOT clearly visible → DO NOT output it.
162
+ 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.
163
+ 4. NO REPETITION: Do NOT repeat characters endlessly. Use condensed form only.
164
+
165
+ ==================================================
166
+ ORDER NO. 2: ONE ELEMENT = ONE LINE (ABSOLUTE LOCK & GROUPING RULES)
167
+ ==================================================
168
+ - 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.
169
+ - 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.
170
+ - NEVER merge bubbles vertically, horizontally, diagonally. Fragment output is FORBIDDEN.
171
+
172
+ ==================================================
173
+ ORDER NO. 3: SENTENCE INTEGRITY
174
+ ==================================================
175
+ - A single sentence MUST stay in ONE tag. NEVER continue a sentence using OT: or any other tag.
176
+
177
+ ==================================================
178
+ ORDER NO. 4: TAG SELECTION (SHAPE-ONLY)
179
+ ==================================================
180
+ Tag selection is STRICTLY based on the VISUAL SHAPE of the bubble's border.
181
+
182
+ 1. (): THOUGHT (STRICT IDENTIFICATION)
183
+ - Visual MUST BE EXACTLY ONE OF THESE TWO:
184
+ A) Connected to the character by a DOTTED TAIL (a trail of small distinct circles).
185
+ 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.
186
+ - Format: `(): Text`
187
+
188
+ 2. %: SCREAM / SHOUT / PAIN
189
+ - Visual: LARGE, IRREGULAR jagged zig-zags, explosion-like shapes, or violently shaky borders.
190
+ - CRITICAL NEGATIVE RULE: Do NOT use `%` if the border consists of fine, dense, uniform, hair-like lines (Lion's Mane). That MUST be `()`.
191
+ - Format: `%: Text`
192
+
193
+ 3. ST: SMALL TEXT
194
+ - Visibly smaller text, floating freely, annotations, or outside any main bubble.
195
+ - CRITICAL: Do NOT default to `""` for text that is clearly smaller than the main dialogue. It MUST be `ST`.
196
+ - Format: `ST: Text`
197
+
198
+ 4. "": SPEECH
199
+ - Smooth oval or rectangular bubble with a normal solid speech tail.
200
+ - Format: `"": Text`
201
+
202
+ 5. //: CONNECTED SPEECH
203
+ - Visually connected continuation bubble. SAME speaker only.
204
+ - Format: `//: Text`
205
+
206
+ 6. []: BOX
207
+ - Physical rectangular narration box, NOT a screen/UI. Multiple lines inside one box MUST be merged into one single `[]` line.
208
+ - Format: `[]: Text`
209
+
210
+ 7. <>: SCREEN SYSTEM
211
+ - Text inside phone screen, game screen, system UI, HUD. Treat the entire screen text block as ONE single `<>` line.
212
+ - Format: `<>: Text`
213
+
214
+ 8. OT: ARTWORK / BACKGROUND TEXT
215
+ - Large text written directly on artwork, NO bubble/box. Group adjacent lines together!
216
+ - Format: `OT: Text`
217
+
218
+ 9. SFX: SOUND EFFECTS
219
+ - Stylized non-verbal sound lettering.
220
+ - Format: `SFX: Text`
221
+
222
+ ==================================================
223
+ ORDER NO. 5: STRICT READING DIRECTION & SEQUENCE (CRITICAL)
224
+ ==================================================
225
+ 1. PRIMARY RULE (HEIGHT): ALWAYS read from TOP to BOTTOM. The element physically higher on the page comes first.
226
+ 2. SECONDARY RULE (TIE-BREAKER): If two separate elements are at the exact same height, read RIGHT to LEFT.
227
+ 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 `//:`.
228
+
229
+ ==================================================
230
+ ORDER NO. 6: FINAL ENFORCEMENT (ZERO TOLERANCE)
231
+ ==================================================
232
+ - NO translation, NO hallucination, NO missing text, NO merged bubbles, NO split bubbles.
233
+ - ONE tag per line. ONE bubble per line. ALL text must be tagged.
234
+ - STRICT sequence must be followed.
235
+ - IF NO TEXT IS VISIBLE IN THE IMAGE, RETURN EXACTLY: `OT: [No Text]`
236
+ """
237
+
238
+ safety_settings = [{"category": c, "threshold": "BLOCK_NONE"} for c in [
239
+ "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_HATE_SPEECH",
240
+ "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_DANGEROUS_CONTENT"
241
+ ]]
242
+
243
+ gen_config = {"temperature": 0.0, "max_output_tokens": 8192, "top_p": 0.9}
244
+ model_name = db.get_bot_setting("advance_model" if advance_mode else "normal_model")
245
+
246
+ for attempt in range(len(self.keys)):
247
+ current_api_key = self.get_valid_key()
248
+ if not current_api_key:
249
+ await asyncio.sleep(5)
250
+ continue
251
+
252
+ genai.configure(api_key=current_api_key)
253
+ await asyncio.sleep(random.uniform(1.0, 2.5))
254
+
255
+ try:
256
+ model = genai.GenerativeModel(model_name, safety_settings=safety_settings)
257
+ with open(image_path, "rb") as f: img = f.read()
258
+
259
+ response = await asyncio.wait_for(
260
+ asyncio.to_thread(
261
+ model.generate_content,
262
+ [prompt, {"mime_type": "image/jpeg", "data": img}],
263
+ generation_config=gen_config
264
+ ), timeout=45.0
265
+ )
266
+
267
+ if response.text: return response.text
268
+
269
+ except asyncio.TimeoutError:
270
+ continue
271
+ except Exception as e:
272
+ error_msg = str(e).lower()
273
+ if "not found" in error_msg or "404" in error_msg or "invalid" in error_msg: continue
274
+ if "429" in error_msg or "quota" in error_msg or "exhausted" in error_msg:
275
+ self.report_dead_key(current_api_key)
276
+ await asyncio.sleep(1.5)
277
+ continue
278
+ if "safety" in error_msg or "finish_reason" in error_msg: break
279
+ continue
280
+
281
+ return ""
282
+
283
+ ai_engine = GeminiManager()
284
+
285
+ # ================================
286
+ # 🧹 5. Clean Text (Anti-Hallucination Filter)
287
+ # ================================
288
+ def clean_text(raw, settings):
289
+ try:
290
+ add_blank = settings.get("blank_line", True)
291
+ sfx_on = settings.get("include_sfx", False)
292
+ c = settings.get("custom_tags", {})
293
+
294
+ tag_map = {k: c.get(k, k) for k in ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%']}
295
+ base_defaults = ['""', '//', '()', 'ST', 'OT', 'SFX', '[]', '<>', '%']
296
+
297
+ all_tags = {d: d for d in base_defaults}
298
+ for k, v in c.items(): all_tags[v] = k
299
+
300
+ hallucination_triggers = [
301
+ "TASK: STRICT OCR", "CORE PRINCIPLES", "ORDER NO. 1",
302
+ "ORDER NO. 2", "ORDER NO. 3", "ORDER NO. 4", "ORDER NO. 5",
303
+ "ORDER NO. 6", "VISUAL SHAPE ONLY", "Format:"
304
+ ]
305
+
306
+ out = []
307
+ for line in raw.split('\n'):
308
+ line = line.strip()
309
+
310
+ if not line or re.match(r'^(I|II|III|IV|V|VI|VII|VIII|IX|X)-\s', line, re.IGNORECASE): continue
311
+ if any(trigger.lower() in line.lower() for trigger in hallucination_triggers): continue
312
+ if "panel" in line.lower() or "bubble" in line.lower(): continue
313
+
314
+ line = re.sub(r'^(?:OT:\s*)?\[OT:\s*(.*?)\]?$', r'OT: \1', line)
315
+ line = re.sub(r'^OT:\s*OT:\s*', r'OT: ', line)
316
+ line = line.replace("<:>", "<>:").replace("< : >", "<>:")
317
+ line = re.sub(r'(.)\1{6,}', r'\1\1...', line)
318
+ line = line.replace("Million", "milyun").replace("million", "milyun")
319
+
320
+ detected_tag = None
321
+ content = line
322
+
323
+ for tag_str in sorted(all_tags.keys(), key=len, reverse=True):
324
+ if line.startswith(f"{tag_str}:") or line.startswith(tag_str):
325
+ detected_tag = all_tags[tag_str]
326
+ content = line[len(tag_str)+1:].strip() if line.startswith(f"{tag_str}:") else line[len(tag_str):].strip()
327
+ break
328
+
329
+ if detected_tag:
330
+ if detected_tag == 'SFX' and not sfx_on: continue
331
+ content = content.strip().strip('"').strip("'").strip("`").strip()
332
+ if detected_tag == '()' and content.startswith('(') and content.endswith(')'):
333
+ content = content[1:-1].strip()
334
+
335
+ if len(content) > 600 and " " not in content: content = content[:50] + "..."
336
+ final_tag = tag_map.get(detected_tag, detected_tag)
337
+ out.append(f"{final_tag}: {content}")
338
+ else:
339
+ clean = line.strip().strip('"').strip("'").strip("`").strip()
340
+ if clean:
341
+ if clean.startswith(":") or clean.startswith(":"):
342
+ clean_speech = clean[1:].strip()
343
+ out.append(f"{tag_map.get('\"\"', '\"\"')}: {clean_speech}")
344
+ else:
345
+ if out:
346
+ out[-1] += f" {clean}"
347
+ else:
348
+ final_ot = tag_map.get('OT', 'OT')
349
+ if not any(clean.startswith(v) for v in tag_map.values()): out.append(f"{final_ot}: {clean}")
350
+ else: out.append(clean)
351
+
352
+ final_out = []
353
+ for item in out:
354
+ final_out.append(item)
355
+ if add_blank: final_out.append("")
356
+
357
+ return final_out
358
+ except: return [raw]
359
+
360
+ # ================================
361
+ # 🕹️ 6. UI Components (Modals & Views)
362
+ # ================================
363
+ class CancelView(View):
364
+ def __init__(self, user_id, task_id, total_pages):
365
+ super().__init__(timeout=None)
366
+ self.user_id = user_id
367
+ self.task_id = task_id
368
+ self.total_pages = total_pages
369
+
370
+ @discord.ui.button(label="🛑 Cancel Process", style=discord.ButtonStyle.danger, custom_id="cancel_btn")
371
+ async def stop(self, interaction: discord.Interaction, button: Button):
372
+ if interaction.user.id != self.user_id: return
373
+
374
+ current_progress = active_tasks.get(f"{self.task_id}_progress", 0)
375
+ if self.total_pages > 0 and (current_progress / self.total_pages) >= 0.5:
376
+ await interaction.response.send_message("❌ لا يمكن الإلغاء الآن، تم الانتهاء من أكثر من 50% من العمل.", ephemeral=True)
377
+ button.disabled = True
378
+ await interaction.message.edit(view=self)
379
+ return
380
+
381
+ if self.task_id in active_tasks:
382
+ active_tasks[self.task_id].cancel()
383
+ del active_tasks[self.task_id]
384
+ button.disabled = True
385
+ button.label = "تم الإلغاء ⛔"
386
+ await interaction.response.edit_message(view=self)
387
+ await interaction.followup.send("⛔ تم إلغاء العملية بنجاح. لن يتم خصم المزيد.", ephemeral=True)
388
+
389
+ class FeedbackModal(Modal, title="📝 تقييم جودة الاستخراج (Feedback)"):
390
+ feedback_text = TextInput(label='رأيك في دقة النصوص وملاحظاتك', style=discord.TextStyle.paragraph, placeholder='اكتب مشكلتك أو اقتراحك هنا...', required=True, max_length=1000)
391
+
392
+ async def on_submit(self, interaction: discord.Interaction):
393
+ await interaction.response.send_message("✅ شكراً لتقييمك! تم إرسال ملاحظاتك للإدارة لتطوير البوت.", ephemeral=True)
394
+ embed = discord.Embed(title="📝 تقييم جديد للاستخراج", color=0x3498db, timestamp=datetime.utcnow())
395
+ embed.add_field(name="المستخدم", value=interaction.user.mention, inline=False)
396
+ embed.add_field(name="التقييم/المشكلة", value=self.feedback_text.value, inline=False)
397
+ try: await send_log(interaction.guild, embed)
398
+ except: pass
399
+
400
+ class PostExtractView(View):
401
+ def __init__(self):
402
+ super().__init__(timeout=None)
403
+
404
+ @discord.ui.button(label="📝 إبلاغ عن مشكلة / اقتراح", style=discord.ButtonStyle.secondary, custom_id="feedback_btn")
405
+ async def feedback(self, interaction: discord.Interaction, button: Button):
406
+ await interaction.response.send_modal(FeedbackModal())
407
+ # ================================
408
+ # 🗄️ 7. Database & Auto-Backup System
409
+ # ================================
410
+ class UserDatabase:
411
+ def __init__(self):
412
+ self.lock = threading.Lock()
413
+ self.db = self.load_db()
414
+ self.init_bot_config()
415
+
416
+ def load_db(self):
417
+ if os.path.exists(USERS_DB_FILE):
418
+ try: return json.load(open(USERS_DB_FILE, 'r', encoding='utf-8'))
419
+ except: return {}
420
+ return {}
421
+
422
+ def save_db(self):
423
+ try:
424
+ with open(USERS_DB_FILE, 'w', encoding='utf-8') as f:
425
+ json.dump(self.db, f, indent=4, ensure_ascii=False)
426
+ except: pass
427
+
428
+ def init_bot_config(self):
429
+ with self.lock:
430
+ if "bot_config" not in self.db:
431
+ self.db["bot_config"] = {"advance_model": "gemini-2.5-flash", "normal_model": "gemini-2.5-flash", "bot_admins": []}
432
+ self.save_db()
433
+
434
+ def is_bot_admin(self, user_id):
435
+ return user_id in self.db.get("bot_config", {}).get("bot_admins", [])
436
+
437
+ def add_bot_admin(self, user_id):
438
+ with self.lock:
439
+ if user_id not in self.db["bot_config"]["bot_admins"]:
440
+ self.db["bot_config"]["bot_admins"].append(user_id)
441
+ self.save_db()
442
+
443
+ def get_bot_setting(self, key):
444
+ with self.lock:
445
+ self.db = self.load_db()
446
+ return self.db.get("bot_config", {}).get(key, "gemini-2.5-flash")
447
+
448
+ def update_bot_setting(self, key, value):
449
+ with self.lock:
450
+ self.db = self.load_db()
451
+ if "bot_config" not in self.db: self.db["bot_config"] = {}
452
+ self.db["bot_config"][key] = value
453
+ self.save_db()
454
+
455
+ def get_user(self, user_id, username):
456
+ with self.lock:
457
+ self.db = self.load_db()
458
+ uid = str(user_id)
459
+ if uid not in self.db:
460
+ self.db[uid] = {
461
+ "name": username, "points": 0.0, "registered": False, "rank": "Normal",
462
+ "images_extracted": 0, "points_spent": 0.0, "free_merges": 3,
463
+ "settings": {"format": "docx", "blank_line": True, "include_sfx": False, "language": "ar", "advance_mode": False, "custom_tags": {}}
464
+ }
465
+ self.save_db()
466
+ return self.db[uid]
467
+
468
+ def modify_points(self, user_id, amount, spent=False):
469
+ with self.lock:
470
+ self.db = self.load_db()
471
+ uid = str(user_id)
472
+ if uid in self.db:
473
+ self.db[uid]["points"] = round(self.db[uid]["points"] + amount, 2)
474
+ if spent and amount < 0:
475
+ self.db[uid]["points_spent"] = round(self.db[uid].get("points_spent", 0) + abs(amount), 2)
476
+ self.save_db()
477
+ return self.db[uid]["points"]
478
+ return 0
479
+
480
+ def add_stats(self, user_id, images_count):
481
+ with self.lock:
482
+ uid = str(user_id)
483
+ if uid in self.db:
484
+ self.db[uid]["images_extracted"] = self.db[uid].get("images_extracted", 0) + images_count
485
+ self.save_db()
486
+
487
+ def update_setting(self, user_id, key, value):
488
+ with self.lock:
489
+ self.db = self.load_db()
490
+ uid = str(user_id)
491
+ if uid in self.db:
492
+ self.db[uid]["settings"][key] = value
493
+ self.save_db()
494
+
495
+ db = UserDatabase()
496
+
497
+ # الحفظ الاحتياطي السحابي (كل ساعة) لضمان عدم ضياع البيانات في Hugging Face
498
+ @tasks.loop(hours=1)
499
+ async def backup_task():
500
+ try:
501
+ log_channel = bot.get_channel(LOG_CHANNEL_ID)
502
+ if log_channel and os.path.exists(USERS_DB_FILE):
503
+ async for msg in log_channel.history(limit=10):
504
+ if msg.author == bot.user and "💾 النسخة الاحتياطية" in msg.content:
505
+ try: await msg.delete()
506
+ except: pass
507
+
508
+ # رفع نسخة من قاعدة البيانات وملف التراكر
509
+ files = [discord.File(USERS_DB_FILE)]
510
+ if os.path.exists(TRACKER_DB_FILE): files.append(discord.File(TRACKER_DB_FILE))
511
+ await log_channel.send("💾 النسخة الاحتياطية التلقائية لقاعدة البيانات (لا تقم بمسح هذه الرسالة)", files=files)
512
+ except Exception as e:
513
+ print(f"Backup Error: {e}")
514
+
515
+ async def send_log(guild, embed, files=None):
516
+ if not LOG_CHANNEL_ID: return
517
+ channel = guild.get_channel(LOG_CHANNEL_ID)
518
+ if channel:
519
+ try: await channel.send(embed=embed, files=files or [])
520
+ except: pass
521
+
522
+ # ================================
523
+ # 🖼️ 8. Image Toolbox (Smart Stitching & Splitting)
524
+ # ================================
525
+ class ImageToolbox:
526
+ @staticmethod
527
+ def find_blank_y(img, start_y, end_y):
528
+ # خوارزمية ذكية للبحث عن مساحة بيضاء (فراغ بين الفقاعات) لتفادي القص الخاطئ
529
+ try:
530
+ gray = img.convert('L')
531
+ best_y = start_y
532
+ min_variance = float('inf')
533
+ for y in range(start_y, end_y, 10): # قفزات 10 بيكسل لتسريع العملية
534
+ row = [gray.getpixel((x, y)) for x in range(gray.width)]
535
+ variance = max(row) - min(row)
536
+ if variance < min_variance:
537
+ min_variance = variance
538
+ best_y = y
539
+ if min_variance < 5: return y # خط أبيض مثالي
540
+ return best_y
541
+ except: return (start_y + end_y) // 2
542
+
543
+ @staticmethod
544
+ def check_and_split(image_path, is_advance=False):
545
+ try:
546
+ with Image.open(image_path) as img:
547
+ width, height = img.size
548
+
549
+ # الوضع الاحترافي: يقص بدقة أعلى لمعالجة أوضح
550
+ threshold = 4000 if is_advance else 6000
551
+ if height <= threshold:
552
+ return [image_path], False, height
553
+
554
+ mid = height // 2
555
+ scan_range = int(height * 0.15)
556
+ # البحث عن نقطة القص الذكية في المنتصف
557
+ best_cut_y = ImageToolbox.find_blank_y(img, max(0, mid - scan_range), min(height, mid + scan_range))
558
+
559
+ overlap = 20
560
+ top = img.crop((0, 0, width, min(height, best_cut_y + overlap)))
561
+ btm = img.crop((0, max(0, best_cut_y - overlap), width, height))
562
+
563
+ base, ext = os.path.splitext(image_path)
564
+ p1, p2 = f"{base}_p1{ext}", f"{base}_p2{ext}"
565
+ top.save(p1); btm.save(p2)
566
+ return [p1, p2], True, height
567
+ except: return [image_path], False, 0
568
+
569
+ @staticmethod
570
+ def merge_images_vertically(image_paths, output_path, max_height=50000):
571
+ try:
572
+ imgs = [Image.open(p).convert('RGB') for p in image_paths]
573
+ if not imgs: return False
574
+ base_w = max(im.width for im in imgs)
575
+ parts, curr_imgs, curr_h = [], [], 0
576
+
577
+ for img in imgs:
578
+ # توحيد العرض مع الحفاظ على النسبة والتناسب
579
+ if img.width != base_w:
580
+ img = img.resize((base_w, int(img.height * (base_w / img.width))), Image.Resampling.LANCZOS)
581
+
582
+ # نظام الدمج الذكي لتفادي كسر الـ max_height بشكل غبي
583
+ if curr_h + img.height > max_height and curr_imgs:
584
+ parts.append((curr_imgs, base_w, curr_h))
585
+ curr_imgs, curr_h = [], 0
586
+
587
+ curr_imgs.append(img)
588
+ curr_h += img.height
589
+
590
+ if curr_imgs: parts.append((curr_imgs, base_w, curr_h))
591
+
592
+ out_files = []
593
+ for idx, (p_imgs, w, h) in enumerate(parts):
594
+ canv = Image.new('RGB', (w, h), (255, 255, 255))
595
+ y_off = 0
596
+ for im in p_imgs:
597
+ canv.paste(im, (0, y_off))
598
+ y_off += im.height
599
+
600
+ fname = output_path.replace(".jpg", f"_part{idx+1}.jpg") if len(parts) > 1 else output_path
601
+ canv.save(fname, quality=90)
602
+ out_files.append(fname)
603
+ return out_files
604
+ except Exception as e:
605
+ print(f"Merge Error: {e}")
606
+ return False
607
+
608
+ # ================================
609
+ # 📥 9. Universal Downloader & Advanced Web Scraper
610
+ # ================================
611
+ class DownloadManager:
612
+ @staticmethod
613
+ def extract_archives(folder):
614
+ for root, _, files in os.walk(folder):
615
+ for file in files:
616
+ fp = os.path.join(root, file)
617
+ try:
618
+ if file.lower().endswith(".zip"):
619
+ with zipfile.ZipFile(fp, 'r') as z: z.extractall(root)
620
+ os.remove(fp)
621
+ elif file.lower().endswith((".rar", ".cbr", ".cbz")):
622
+ patoolib.extract_archive(fp, outdir=root, verbosity=-1)
623
+ os.remove(fp)
624
+ except: pass
625
+ for root, dirs, files in os.walk(folder):
626
+ for name in files:
627
+ if name.startswith('.') or name.startswith('__MACOSX'):
628
+ try: os.remove(os.path.join(root, name))
629
+ except: pass
630
+
631
+ @staticmethod
632
+ def scrape_web_selenium(url, output_folder):
633
+ # 🚀 تحديث جذري لسحب مواقع الويب وحل مشكلة صورتين فقط (Lazy Loading Fix)
634
+ driver = None
635
+ try:
636
+ chrome_options = Options()
637
+ chrome_options.add_argument('--headless=new')
638
+ chrome_options.add_argument('--no-sandbox')
639
+ chrome_options.add_argument('--disable-dev-shm-usage')
640
+ 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")
641
+
642
+ service = Service(ChromeDriverManager().install())
643
+ driver = webdriver.Chrome(service=service, options=chrome_options)
644
+ driver.get(url)
645
+ time.sleep(5)
646
+
647
+ # النزول البطيء والمستمر لتحفيز تحميل الصور
648
+ last_h = driver.execute_script("return document.body.scrollHeight")
649
+ for _ in range(60): # محاولة النزول 60 مرة
650
+ driver.execute_script("window.scrollBy(0, 1000);")
651
+ time.sleep(1.2) # انتظار كافٍ لرد السيرفر
652
+ new_h = driver.execute_script("return document.body.scrollHeight")
653
+ if new_h == last_h:
654
+ time.sleep(3) # محاولة أخيرة لو توقف النزول
655
+ new_h = driver.execute_script("return document.body.scrollHeight")
656
+ if new_h == last_h: break
657
+ last_h = new_h
658
+
659
+ time.sleep(3)
660
+
661
+ # استخراج الصور بذكاء (تجاهل الأيقونات والبنرات)
662
+ elems = driver.find_elements(By.TAG_NAME, 'img')
663
+ urls = []
664
+ for e in elems:
665
+ src = e.get_attribute('src') or e.get_attribute('data-src') or e.get_attribute('data-original')
666
+ if src and src.startswith('http') and not any(x in src.lower() for x in ['logo','ad','banner','icon','avatar', 'profile']):
667
+ urls.append(src)
668
+
669
+ # إزالة التكرار مع الحفاظ على الترتيب الأصلي
670
+ urls = list(dict.fromkeys(urls))
671
+
672
+ saved_paths = []
673
+ req_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}
674
+
675
+ for idx, u in enumerate(urls):
676
+ try:
677
+ c = requests.get(u, headers=req_headers, timeout=15).content
678
+ im = Image.open(io.BytesIO(c)).convert('RGB')
679
+ # التأكد من أن الصورة هي صفحة مانهوا فعلية (أبعاد كبيرة) وليست أيقونة صغيرة
680
+ if im.width > 300 and im.height > 300:
681
+ path = os.path.join(output_folder, f"page_{idx:03d}.jpg")
682
+ im.save(path)
683
+ saved_paths.append(path)
684
+ except: pass
685
+
686
+ title = driver.title.split('|')[0].strip() if driver.title else "Web_Chapter"
687
+ title = re.sub(r'[\\/*?:"<>|]', "", title)
688
+ driver.quit()
689
+ return saved_paths, title
690
+ except Exception as e:
691
+ if driver: driver.quit()
692
+ print(f"Scraper Error: {e}")
693
+ return [], "Error"
694
+
695
+ @staticmethod
696
+ async def download(url, output_folder):
697
+ success = False
698
+ name = "Downloaded_Content"
699
+
700
+ # التوجه مباشرة لخطة الطوارئ gdown لتحميل الملفات
701
+ if "drive.google.com" in url:
702
+ try:
703
+ if "folder" in url or "drive.google.com/drive/folders" in url:
704
+ res = await asyncio.to_thread(gdown.download_folder, url, output=output_folder, quiet=False, use_cookies=False)
705
+ if res: success = True
706
+ else:
707
+ res = await asyncio.to_thread(gdown.download, url, output=os.path.join(output_folder, "gdown_fallback.zip"), quiet=False, fuzzy=True)
708
+ if res: success = True
709
+ if success: name = "Drive_Content"
710
+ except Exception as e: print(f"Drive Error: {e}")
711
+ else:
712
+ try:
713
+ async with aiohttp.ClientSession() as s:
714
+ async with s.get(url, headers={'User-Agent': 'Mozilla/5.0'}) as r:
715
+ if r.status == 200:
716
+ cd = r.headers.get('Content-Disposition', '')
717
+ if 'filename=' in cd: name = re.findall("filename=(.+)", cd)[0].strip('"')
718
+ else: name = "file.zip"
719
+ with open(os.path.join(output_folder, name), 'wb') as f: f.write(await r.read())
720
+ success = True
721
+ except: pass
722
+
723
+ DownloadManager.extract_archives(output_folder)
724
+ has_images = any(f.lower().endswith(('.png','.jpg','.jpeg','.webp')) for r, d, files in os.walk(output_folder) for f in files)
725
+ if success and has_images: return True, name
726
+ return False, "Error downloading or no images found."
727
+
728
+ @staticmethod
729
+ def get_chapters_dict(folder):
730
+ chapters = {}
731
+ subdirs = [d for d in os.listdir(folder) if os.path.isdir(os.path.join(folder, d))]
732
+ if not subdirs:
733
+ imgs = [os.path.join(folder, f) for f in os.listdir(folder) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))]
734
+ # هنا نستخدم دالة natural_sort_key التي وضعناها في الجزء الأول لترتيب الصور بشكل بشري دقيق
735
+ if imgs: chapters["Main"] = sorted(imgs, key=natural_sort_key)
736
+ else:
737
+ for d in sorted(subdirs, key=natural_sort_key):
738
+ d_path = os.path.join(folder, d)
739
+ imgs = [os.path.join(d_path, f) for f in os.listdir(d_path) if f.lower().endswith(('.png','.jpg','.jpeg','.webp'))]
740
+ if imgs: chapters[d] = sorted(imgs, key=natural_sort_key)
741
+ return chapters
742
+
743
+ # ================================
744
+ # 🔗 10. Universal Uploader (Gofile + Catbox Fallback)
745
+ # ================================
746
+ class FileUploader:
747
+ @staticmethod
748
+ async def upload(file_path):
749
+ # المحاولة الأولى: Gofile
750
+ try:
751
+ server_res = await asyncio.to_thread(requests.get, "https://api.gofile.io/servers")
752
+ server = server_res.json()['data']['servers'][0]['name']
753
+ with open(file_path, 'rb') as f:
754
+ upload_res = await asyncio.to_thread(requests.post, f"https://{server}.gofile.io/contents/upload", files={'file': f})
755
+ return upload_res.json()['data']['downloadPage']
756
+ except Exception as e:
757
+ print(f"⚠️ فشل الرفع على Gofile، جاري التحويل إلى Catbox... ({e})")
758
+
759
+ # المحاولة الثانية (Fallback): Catbox (سريع وموثوق جداً)
760
+ try:
761
+ with open(file_path, 'rb') as f:
762
+ res = await asyncio.to_thread(requests.post, "https://catbox.moe/user/api.php", data={"reqtype": "fileupload"}, files={"fileToUpload": f})
763
+ if res.status_code == 200: return res.text
764
+ except Exception as e:
765
+ print(f"❌ فشل الرفع على جميع السيرفرات: {e}")
766
+
767
+ return None
768
+
769
+ # ================================
770
+ # 🕹️ 11. UI Components (OCR specific views)
771
+ # ================================
772
+ class CostConfirmView(View):
773
+ def __init__(self, user_id, cost, lang, on_confirm_callback, tmp_dir):
774
+ super().__init__(timeout=86400)
775
+ self.user_id = user_id
776
+ self.callback = on_confirm_callback
777
+ self.tmp_dir = tmp_dir
778
+
779
+ self.btn_yes.label = "✅ استكمال (نعم)" if lang == "ar" else "✅ Proceed (Yes)"
780
+ self.btn_no.label = "❌ إلغاء (لا)" if lang == "ar" else "❌ Cancel (No)"
781
+
782
+ @discord.ui.button(style=discord.ButtonStyle.success, custom_id="btn_yes")
783
+ async def btn_yes(self, interaction: discord.Interaction, button: Button):
784
+ if interaction.user.id != self.user_id: return
785
+ for child in self.children: child.disabled = True
786
+ await interaction.response.edit_message(content=f"{interaction.user.mention} ⏳ يتم التجهيز للاستخراج...", embed=None, view=None)
787
+ await self.callback(interaction)
788
+ self.stop()
789
+
790
+ @discord.ui.button(style=discord.ButtonStyle.danger, custom_id="btn_no")
791
+ async def btn_no(self, interaction: discord.Interaction, button: Button):
792
+ if interaction.user.id != self.user_id: return
793
+ for child in self.children: child.disabled = True
794
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
795
+ await interaction.response.edit_message(content=f"{interaction.user.mention} ⛔ تم الإلغاء وحذف الملفات المؤقتة.", embed=None, view=None)
796
+ self.stop()
797
+
798
+ async def on_timeout(self):
799
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
800
+
801
+ class ChapterSelectView(View):
802
+ def __init__(self, user_id, chapters_dict, on_select_callback, tmp_dir):
803
+ super().__init__(timeout=86400)
804
+ self.user_id = user_id
805
+ self.chapters_dict = chapters_dict
806
+ self.callback = on_select_callback
807
+ self.tmp_dir = tmp_dir
808
+
809
+ options = []
810
+ for name, imgs in list(chapters_dict.items())[:24]:
811
+ options.append(discord.SelectOption(label=f"فصل: {name[:50]}", description=f"{len(imgs)} صور", value=name))
812
+ options.append(discord.SelectOption(label="🌟 تحديد الكل", description="سحب كل الفصول", value="ALL"))
813
+
814
+ self.select = Select(placeholder="اختر الفصول المراد معالجتها...", min_values=1, max_values=len(options), options=options)
815
+ self.select.callback = self.select_callback
816
+ self.add_item(self.select)
817
+
818
+ async def select_callback(self, interaction: discord.Interaction):
819
+ if interaction.user.id != self.user_id: return
820
+ self.select.disabled = True
821
+ await interaction.response.edit_message(content="✅ تم الاختيار. جاري الحساب...", view=None)
822
+
823
+ selected_chaps = {}
824
+ if "ALL" in self.select.values: selected_chaps = self.chapters_dict
825
+ else:
826
+ for val in self.select.values: selected_chaps[val] = self.chapters_dict[val]
827
+
828
+ await self.callback(interaction, selected_chaps)
829
+ self.stop()
830
+
831
+ async def on_timeout(self):
832
+ shutil.rmtree(self.tmp_dir, ignore_errors=True)
833
+
834
+ # ================================
835
+ # 🧩 12. Merge Command (/merge)
836
+ # ================================
837
+ @bot.tree.command(name="merge", description="دمج الصور بذكاء ورفعها (أول 3 مرات مجاناً)")
838
+ @app_commands.describe(max_height="أقصى طول للصورة المدمجة (الافتراضي 50000 بيكسل)")
839
+ async def merge_cmd(interaction: discord.Interaction, link: str, max_height: int = 50000):
840
+ user = db.get_user(interaction.user.id, interaction.user.name)
841
+
842
+ is_free = False
843
+ with db.lock:
844
+ if db.db[str(interaction.user.id)].get("free_merges", 0) > 0: is_free = True
845
+ elif db.db[str(interaction.user.id)]["points"] < 0.1:
846
+ return await interaction.response.send_message("❌ رصيدك لا يكفي (مطلوب 0.1 نقطة لعملية الدمج).", ephemeral=True)
847
+
848
+ await interaction.response.send_message(embed=discord.Embed(title="⏳ جاري سحب الصور للدمج...", color=0x9b59b6))
849
+ msg = await interaction.original_response()
850
+
851
+ bot_queue["merge"].append(interaction.user.name)
852
+ tmp = os.path.join(TEMP_ROOT, f"merge_{interaction.user.id}_{int(time.time())}")
853
+ os.makedirs(tmp, exist_ok=True)
854
+
855
+ try:
856
+ success, source_name = await DownloadManager.download(link, tmp)
857
+ if not success:
858
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
859
+ return await msg.edit(content="❌ فشل سحب الصور من الرابط.")
860
+
861
+ chapters_dict = DownloadManager.get_chapters_dict(tmp)
862
+ if not chapters_dict:
863
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
864
+ return await msg.edit(content="❌ لم يتم العثور على صور صالحة للدمج.")
865
+
866
+ imgs = list(chapters_dict.values())[0]
867
+ if len(imgs) < 2:
868
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
869
+ return await msg.edit(content="❌ الرابط يحتوي على أقل من صورتين.")
870
+
871
+ await msg.edit(embed=discord.Embed(title=f"🛠️ جاري دمج الصور بذكاء (الطول الأقصى: {max_height})...", color=0xf1c40f))
872
+
873
+ merged_files = await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, os.path.join(tmp, f"{source_name}_merged.jpg"), max_height=max_height)
874
+
875
+ if not merged_files:
876
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
877
+ return await msg.edit(content="❌ فشلت عملية الدمج.")
878
+
879
+ await msg.edit(embed=discord.Embed(title="☁️ جاري الرفع للسحابة...", color=0x3498db))
880
+
881
+ zip_path = os.path.join(tmp, f"{source_name}_Merged.zip")
882
+ with zipfile.ZipFile(zip_path, 'w') as z:
883
+ for f in merged_files: z.write(f, os.path.basename(f))
884
+
885
+ upload_link = await FileUploader.upload(zip_path)
886
+
887
+ if not upload_link:
888
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
889
+ return await msg.edit(content="❌ فشل الرفع على Gofile والسيرفرات البديلة، تم إلغاء العملية ولم يتم خصم نقاط.")
890
+
891
+ cost_msg = ""
892
+ with db.lock:
893
+ if is_free:
894
+ db.db[str(interaction.user.id)]["free_merges"] -= 1
895
+ rem_free = db.db[str(interaction.user.id)]["free_merges"]
896
+ cost_msg = f"🎁 **مجاناً** (تبقى لك {rem_free} مرات مجانية)"
897
+ else:
898
+ db.db[str(interaction.user.id)]["points"] = round(db.db[str(interaction.user.id)]["points"] - 0.1, 2)
899
+ db.db[str(interaction.user.id)]["points_spent"] = round(db.db[str(interaction.user.id)].get("points_spent", 0) + 0.1, 2)
900
+ cost_msg = f"💰 **تم خصم 0.1 نقطة** (رصيدك: {db.db[str(interaction.user.id)]['points']} نقطة)"
901
+ db.save_db()
902
+
903
+ res_embed = discord.Embed(title="✅ تم الدمج والرفع بنجاح!", color=0x2ecc71)
904
+ res_embed.add_field(name="الرابط:", value=f"[اضغط هنا لتحميل الصور المدمجة]({upload_link})", inline=False)
905
+ res_embed.add_field(name="التكلفة:", value=cost_msg, inline=False)
906
+ await msg.edit(content=f"{interaction.user.mention}", embed=res_embed)
907
+
908
+ except Exception as e:
909
+ await msg.edit(content=f"⚠️ حدث خطأ أثناء الدمج: {e}")
910
+ finally:
911
+ if interaction.user.name in bot_queue["merge"]: bot_queue["merge"].remove(interaction.user.name)
912
+ shutil.rmtree(tmp, ignore_errors=True)
913
+
914
+ # ================================
915
+ # 🚀 13. Core Extraction Commands (/ocr)
916
+ # ================================
917
+ def get_rank_multiplier(user):
918
+ rank = user.get("rank", "Normal").lower()
919
+ if rank == "vvip": return 0.50
920
+ if rank == "vip": return 0.75
921
+ return 1.0
922
+
923
+ def calculate_initial_cost(user, original_img_count):
924
+ if original_img_count == 1: cost = 0.1
925
+ elif original_img_count <= 15: cost = 1.0
926
+ else: cost = 1.0 + ((original_img_count - 15) * 0.1)
927
+ return round(max(0.01, cost * get_rank_multiplier(user)), 2)
928
+
929
+ def generate_progress_bar(current, total, length=15):
930
+ if total == 0: return f"[{'░' * length}]"
931
+ percent = current / total
932
+ filled = int(length * percent)
933
+ return f"[{'█' * filled}{'░' * (length - filled)}]"
934
+
935
+ @bot.tree.command(name="ocr", description="استخراج النص من المانجا")
936
+ async def ocr_cmd(interaction: discord.Interaction, link: str = None, file: discord.Attachment = None):
937
+ if not link and not file: return await interaction.response.send_message("❌ يرجى توفير رابط أو إرفاق ملف.", ephemeral=True)
938
+ user = db.get_user(interaction.user.id, interaction.user.name)
939
+
940
+ if user["points"] < 0.1:
941
+ return await interaction.response.send_message("❌ رصيدك غير كافٍ للاستخراج.", ephemeral=True)
942
+
943
+ await interaction.response.send_message(embed=discord.Embed(title="⏳ المرحلة 1: جاري التحميل...", color=0xf1c40f))
944
+ msg = await interaction.original_response()
945
+
946
+ bot_queue["ocr"].append(interaction.user.name)
947
+ start_time = time.time()
948
+ task_id = f"ocr_{interaction.user.id}_{int(start_time)}"
949
+ tmp = os.path.join(TEMP_ROOT, task_id)
950
+ os.makedirs(tmp, exist_ok=True)
951
+
952
+ source_name = "Extracted_Content"
953
+ try:
954
+ dl_start = time.time()
955
+ if file:
956
+ path = os.path.join(tmp, file.filename)
957
+ await file.save(path)
958
+ DownloadManager.extract_archives(tmp)
959
+ source_name = file.filename.replace(".zip", "").replace(".rar", "").replace(".cbz", "")
960
+ else:
961
+ success, error_msg = await DownloadManager.download(link, tmp)
962
+ if not success:
963
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
964
+ shutil.rmtree(tmp, ignore_errors=True)
965
+ return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط.")
966
+ source_name = error_msg
967
+ dl_time = time.time() - dl_start
968
+
969
+ chapters_dict = DownloadManager.get_chapters_dict(tmp)
970
+ if not chapters_dict:
971
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
972
+ shutil.rmtree(tmp, ignore_errors=True)
973
+ return await msg.edit(content="❌ لم يتم العثور على صور صالحة للاستخراج.")
974
+
975
+ if len(chapters_dict) > 1:
976
+ info = f"📦 تم العثور على **{len(chapters_dict)}** فصول. يرجى اختيار الفصول المراد معالجتها:"
977
+ async def on_chapter_selected(inter, selected_chapters):
978
+ await start_parallel_extraction(inter, msg, user, selected_chapters, tmp, task_id, dl_time, source_name)
979
+ view = ChapterSelectView(interaction.user.id, chapters_dict, on_chapter_selected, tmp)
980
+ await msg.edit(content=info, embed=None, view=view)
981
+ else:
982
+ await start_parallel_extraction(interaction, msg, user, chapters_dict, tmp, task_id, dl_time, source_name)
983
+
984
+ except Exception as e:
985
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
986
+ await interaction.followup.send(f"⚠️ خطأ غير متوقع: {e}", ephemeral=True)
987
+ shutil.rmtree(tmp, ignore_errors=True)
988
+
989
+ async def start_parallel_extraction(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name):
990
+ task = asyncio.create_task(process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name))
991
+ active_tasks[task_id] = task
992
+ try: await task
993
+ except asyncio.CancelledError: pass
994
+ finally:
995
+ active_tasks.pop(task_id, None); active_tasks.pop(f"{task_id}_progress", None)
996
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
997
+
998
+ async def process_parallel_chapters(interaction, msg, user, chapters_dict, tmp_dir, task_id, dl_time, source_name):
999
+ all_imgs = []
1000
+ for imgs in chapters_dict.values(): all_imgs.extend(imgs)
1001
+ is_advance = user["settings"].get("advance_mode", False)
1002
+ original_img_count = len(all_imgs)
1003
+ total_cost = calculate_initial_cost(user, original_img_count)
1004
+ lang = user["settings"].get("language", "ar")
1005
+
1006
+ async def execute_ocr(inter=interaction):
1007
+ total_cost_deducted = 0.0
1008
+ try:
1009
+ if db.get_user(inter.user.id, inter.user.name)["points"] < total_cost:
1010
+ return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة `{total_cost:.2f}`.", view=None)
1011
+
1012
+ db.modify_points(inter.user.id, -total_cost, spent=True)
1013
+ total_cost_deducted = total_cost
1014
+
1015
+ ocr_start = time.time()
1016
+ total_pages = original_img_count
1017
+ active_tasks[f"{task_id}_progress"] = 0
1018
+
1019
+ def get_ui_embed(curr, total):
1020
+ e = discord.Embed(title="🚀 جاري استخراج النصوص...", color=0xf1c40f)
1021
+ e.add_field(name="الملف/الفصل", value=f"`{source_name}`", inline=False)
1022
+ percent = int((curr/total)*100) if total > 0 else 0
1023
+ e.add_field(name="تقدم الصور", value=f"`{generate_progress_bar(curr, total)}` **{percent}%** ({curr}/{total})", inline=False)
1024
+ return e
1025
+
1026
+ cancel_view = CancelView(inter.user.id, task_id, total_pages)
1027
+ await msg.edit(content=" ", embed=get_ui_embed(0, total_pages), view=cancel_view)
1028
+
1029
+ total_bubbles = 0
1030
+ failed_pages = 0
1031
+ # تقليل السعة لتفادي ضغط API (Load Balancing)
1032
+ semaphore = asyncio.Semaphore(4)
1033
+
1034
+ async def process_single_image(img_path):
1035
+ async with semaphore:
1036
+ # نقل القص إلى خيط منفصل لمنع خنق المعالج (CPU Choke Fix)
1037
+ parts, _, _ = await asyncio.to_thread(ImageToolbox.check_and_split, img_path, is_advance)
1038
+ page_texts = []
1039
+ for p in parts:
1040
+ raw = await ai_engine.analyze_image(p, advance_mode=is_advance)
1041
+ page_texts.append(raw)
1042
+
1043
+ active_tasks[f"{task_id}_progress"] += 1
1044
+ curr_prog = active_tasks[f"{task_id}_progress"]
1045
+ try: await msg.edit(embed=get_ui_embed(curr_prog, total_pages), view=cancel_view)
1046
+ except: pass
1047
+
1048
+ return page_texts
1049
+
1050
+ for chap_name, imgs in chapters_dict.items():
1051
+ tasks = [process_single_image(img_path) for img_path in imgs]
1052
+ results = await asyncio.gather(*tasks)
1053
+
1054
+ res_text = []
1055
+ for idx, page_parts_raw in enumerate(results):
1056
+ page_txt = []
1057
+ is_failed = True
1058
+ for raw in page_parts_raw:
1059
+ if raw.strip():
1060
+ cleaned = clean_text(raw, user["settings"])
1061
+ if cleaned:
1062
+ is_failed = False
1063
+ valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"]
1064
+ total_bubbles += len(valid_lines)
1065
+ page_txt.append("\n".join(cleaned))
1066
+
1067
+ if is_failed: failed_pages += 1
1068
+ combined_page = "\n".join([pt for pt in page_txt if pt.strip()])
1069
+ res_text.append(f"--- Page {idx+1} ---\n{combined_page}")
1070
+
1071
+ full_text = "\n\n".join(res_text)
1072
+ fmt = user["settings"].get("format", "docx")
1073
+ final_chap_name = source_name if chap_name == "Main" else chap_name
1074
+ safe_chap_name = re.sub(r'[\\/*?:"<>|]', "", final_chap_name)
1075
+ out_p = os.path.join(tmp_dir, f"{safe_chap_name}.{fmt}")
1076
+
1077
+ if fmt == "docx":
1078
+ doc = Document()
1079
+ for line in full_text.split('\n'): doc.add_paragraph(line)
1080
+ doc.save(out_p)
1081
+ else:
1082
+ with open(out_p, 'w', encoding='utf-8') as f: f.write(full_text)
1083
+
1084
+ await inter.channel.send(content=f"📄 تم استخراج فصل: **{final_chap_name}**", file=discord.File(out_p))
1085
+
1086
+ db.add_stats(inter.user.id, total_pages)
1087
+ extract_time = time.time() - ocr_start
1088
+ total_time = dl_time + extract_time
1089
+
1090
+ new_balance = db.get_user(inter.user.id, inter.user.name)["points"]
1091
+ stats_msg = f"""✅ **Extraction Complete for {inter.user.mention}!**
1092
+
1093
+ 💰 **{total_cost:.2f} points deducted. New balance: {new_balance:.2f}**
1094
+
1095
+ 📊 **Stats for `{source_name}`:**
1096
+ • **Total images:** `{total_pages}`
1097
+ • **Text extracted:** `{total_bubbles}`
1098
+ • **Failed extractions:** `{failed_pages}`
1099
+
1100
+ ⏱️ **Processing Time:**
1101
+ • **Download:** `{dl_time:.2f}s`
1102
+ • **Extraction:** `{extract_time:.2f}s`
1103
+ • **Total:** `{total_time:.2f}s`"""
1104
+
1105
+ await msg.delete()
1106
+ await inter.channel.send(content=stats_msg, view=PostExtractView())
1107
+
1108
+ except asyncio.CancelledError:
1109
+ if total_cost_deducted > 0:
1110
+ db.modify_points(inter.user.id, total_cost_deducted, spent=False)
1111
+ await inter.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة.")
1112
+ raise
1113
+ except Exception as e:
1114
+ if total_cost_deducted > 0:
1115
+ db.modify_points(inter.user.id, total_cost_deducted, spent=False)
1116
+ await inter.channel.send(f"♻️ خطأ غير متوقع. تم استرداد **{total_cost_deducted:.2f}** نقطة.")
1117
+ await inter.channel.send(f"⚠️ خطأ: {e}")
1118
+ finally:
1119
+ shutil.rmtree(tmp_dir, ignore_errors=True)
1120
+
1121
+ if total_cost > 1.0 and not is_advance:
1122
+ embed = discord.Embed(title="⚠️ تأكيد الفاتورة", description="فصل طويل، راجع التكلفة:", color=0xe67e22)
1123
+ embed.add_field(name="الصور", value=f"`{original_img_count}`", inline=True)
1124
+ embed.add_field(name="التكلفة", value=f"**`{total_cost:.2f}`** نقطة", inline=True)
1125
+ await msg.edit(content=" ", embed=embed, view=CostConfirmView(interaction.user.id, total_cost, lang, execute_ocr, tmp_dir))
1126
+ else:
1127
+ await execute_ocr(interaction)
1128
+
1129
+ # ================================
1130
+ # 🌐 14. Web OCR Command (/ocrweb)
1131
+ # ================================
1132
+ @bot.tree.command(name="ocrweb", description="سحب واستخراج النص من روابط المواقع المحمية")
1133
+ async def ocrweb_cmd(interaction: discord.Interaction, link: str):
1134
+ user = db.get_user(interaction.user.id, interaction.user.name)
1135
+
1136
+ if user["points"] < 0.1:
1137
+ return await interaction.response.send_message("❌ رصيدك لا يكفي (يجب أن يكون 0.1 على الأقل).", ephemeral=True)
1138
+
1139
+ embed = discord.Embed(title="🌐 جاري سحب الصور من الموقع (Web Scraping)...", color=0x9b59b6)
1140
+ await interaction.response.send_message(embed=embed)
1141
+ msg = await interaction.original_response()
1142
+
1143
+ bot_queue["ocr"].append(interaction.user.name)
1144
+ tmp = os.path.join(TEMP_ROOT, f"web_{interaction.user.id}_{int(time.time())}")
1145
+ os.makedirs(tmp, exist_ok=True)
1146
+
1147
+ total_cost_deducted = 0.0
1148
+ try:
1149
+ dl_start = time.time()
1150
+ # استخدام دالة الـ Scraper المحسنة من الجزء الثاني
1151
+ imgs, name = await asyncio.to_thread(DownloadManager.scrape_web_selenium, link, tmp)
1152
+ dl_time = time.time() - dl_start
1153
+
1154
+ if not imgs:
1155
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
1156
+ shutil.rmtree(tmp, ignore_errors=True)
1157
+ return await msg.edit(content="❌ فشل السحب. تأكد من صحة الرابط أو أن الموقع مدعوم.")
1158
+
1159
+ merged_path = os.path.join(tmp, f"{name}_merged.jpg")
1160
+ await asyncio.to_thread(ImageToolbox.merge_images_vertically, imgs, merged_path)
1161
+
1162
+ cost = calculate_initial_cost(user, len(imgs)) + 0.5
1163
+ if user["points"] < cost:
1164
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
1165
+ shutil.rmtree(tmp, ignore_errors=True)
1166
+ return await msg.edit(content=f"❌ رصيدك لا يكفي. التكلفة: `{cost:.2f}` نقطة.")
1167
+
1168
+ db.modify_points(interaction.user.id, -cost, spent=True)
1169
+ total_cost_deducted = cost
1170
+
1171
+ await msg.edit(embed=discord.Embed(title=f"🚀 جاري استخراج {len(imgs)} صور من الموقع...", color=0x3498db))
1172
+
1173
+ is_advance = user["settings"].get("advance_mode", False)
1174
+ semaphore = asyncio.Semaphore(4)
1175
+
1176
+ extract_start = time.time()
1177
+ failed_pages = 0
1178
+ total_bubbles = 0
1179
+
1180
+ async def process_web_img(idx, img_path):
1181
+ async with semaphore:
1182
+ raw = await ai_engine.analyze_image(img_path, advance_mode=is_advance)
1183
+ return idx, raw
1184
+
1185
+ tasks_list = [process_web_img(i, p) for i, p in enumerate(imgs)]
1186
+ results = await asyncio.gather(*tasks_list)
1187
+ # الترتيب حسب الاندكس لضمان عدم اللخبطة
1188
+ results.sort(key=lambda x: x[0])
1189
+
1190
+ res_text = []
1191
+ for idx, raw in results:
1192
+ if not raw.strip():
1193
+ failed_pages += 1
1194
+ res_text.append(f"--- Page {idx+1} ---\n[Failed to extract text]")
1195
+ continue
1196
+
1197
+ cleaned = clean_text(raw, user["settings"])
1198
+ if not cleaned: failed_pages += 1
1199
+ else:
1200
+ valid_lines = [x for x in cleaned if x.strip() and not x.strip() == "OT: [No Text]"]
1201
+ total_bubbles += len(valid_lines)
1202
+
1203
+ combined_page = "\n".join([pt for pt in cleaned if pt.strip()])
1204
+ res_text.append(f"--- Page {idx+1} ---\n{combined_page}")
1205
+
1206
+ doc_path = os.path.join(tmp, f"{name}.docx")
1207
+ doc = Document()
1208
+ for line in ("\n\n".join(res_text)).split('\n'): doc.add_paragraph(line)
1209
+ doc.save(doc_path)
1210
+
1211
+ db.add_stats(interaction.user.id, len(imgs))
1212
+
1213
+ extract_time = time.time() - extract_start
1214
+ total_time = dl_time + extract_time
1215
+ new_balance = db.get_user(interaction.user.id, interaction.user.name)["points"]
1216
+
1217
+ stats_msg = f"""✅ **Extraction Complete for {interaction.user.mention}!**
1218
+
1219
+ 💰 **{cost:.2f} points deducted. New balance: {new_balance:.2f}**
1220
+
1221
+ 📊 **Stats for `{name}`:**
1222
+ • **Total images:** `{len(imgs)}`
1223
+ • **Text extracted:** `{total_bubbles}`
1224
+ • **Failed extractions:** `{failed_pages}`
1225
+
1226
+ ⏱️ **Processing Time:**
1227
+ • **Download:** `{dl_time:.2f}s`
1228
+ • **Extraction:** `{extract_time:.2f}s`
1229
+ • **Total:** `{total_time:.2f}s`"""
1230
+
1231
+ await msg.delete()
1232
+ await interaction.channel.send(content=stats_msg, files=[discord.File(doc_path), discord.File(merged_path)], view=PostExtractView())
1233
+ except Exception as e:
1234
+ if total_cost_deducted > 0:
1235
+ db.modify_points(interaction.user.id, total_cost_deducted, spent=False)
1236
+ await interaction.channel.send(f"♻️ تم استرداد **{total_cost_deducted:.2f}** نقطة بسبب فشل العملية.")
1237
+ await msg.edit(content=f"⚠️ خطأ: {e}", embed=None)
1238
+ finally:
1239
+ if interaction.user.name in bot_queue["ocr"]: bot_queue["ocr"].remove(interaction.user.name)
1240
+ shutil.rmtree(tmp, ignore_errors=True)
1241
+ # ================================
1242
+ # ⚙️ 15. Settings Command & UI
1243
+ # ================================
1244
+ class LegendsMainModal(Modal, title="تخصيص العلامات الأساسية"):
1245
+ speech = TextInput(label='كلام عادي ("")', placeholder='الافتراضي: ""', required=False, max_length=5)
1246
+ thought = TextInput(label='تفكير (())', placeholder='الافتراضي: ()', required=False, max_length=5)
1247
+ scream = TextInput(label='صراخ (%)', placeholder='الافتراضي: %', required=False, max_length=5)
1248
+ connected = TextInput(label='كلام متصل (//)', placeholder='الافتراضي: //', required=False, max_length=5)
1249
+
1250
+ def __init__(self, user_id):
1251
+ super().__init__()
1252
+ self.user_id = user_id
1253
+
1254
+ async def on_submit(self, interaction: discord.Interaction):
1255
+ user = db.get_user(self.user_id, interaction.user.name)
1256
+ c = user["settings"].get("custom_tags", {})
1257
+ if self.speech.value: c['""'] = self.speech.value
1258
+ if self.thought.value: c['()'] = self.thought.value
1259
+ if self.scream.value: c['%'] = self.scream.value
1260
+ if self.connected.value: c['//'] = self.connected.value
1261
+ db.update_setting(self.user_id, "custom_tags", c)
1262
+ await interaction.response.send_message("✅ تم التحديث بنجاح!", ephemeral=True)
1263
+
1264
+ class SettingsView(View):
1265
+ def __init__(self, user_id, user_data):
1266
+ super().__init__(timeout=120)
1267
+ self.user_id = user_id
1268
+ self.settings = user_data["settings"]
1269
+ self.update_components()
1270
+
1271
+ def update_components(self):
1272
+ for child in self.children:
1273
+ if isinstance(child, discord.ui.Button):
1274
+ if child.custom_id == "blk":
1275
+ child.style = discord.ButtonStyle.success if self.settings.get("blank_line", True) else discord.ButtonStyle.secondary
1276
+ elif child.custom_id == "sfx":
1277
+ child.style = discord.ButtonStyle.success if self.settings.get("include_sfx", False) else discord.ButtonStyle.secondary
1278
+ elif child.custom_id == "adv":
1279
+ child.style = discord.ButtonStyle.success if self.settings.get("advance_mode", False) else discord.ButtonStyle.secondary
1280
+
1281
+ @discord.ui.select(options=[
1282
+ discord.SelectOption(label="تعديل العلامات الأساسية", value="main")
1283
+ ], placeholder="تخصيص علاماتك الخاصة (Custom Tags)")
1284
+ async def legend_select(self, interaction: discord.Interaction, select: Select):
1285
+ if interaction.user.id != self.user_id: return
1286
+ await interaction.response.send_modal(LegendsMainModal(self.user_id))
1287
+
1288
+ @discord.ui.button(label="مسافات بين السطور", custom_id="blk", row=2)
1289
+ async def blank_btn(self, interaction: discord.Interaction, button: Button):
1290
+ if interaction.user.id != self.user_id: return
1291
+ val = not self.settings.get("blank_line", True)
1292
+ db.update_setting(self.user_id, "blank_line", val)
1293
+ self.settings["blank_line"] = val
1294
+ self.update_components()
1295
+ await interaction.response.edit_message(view=self)
1296
+
1297
+ @discord.ui.button(label="استخراج الـ SFX", custom_id="sfx", row=2)
1298
+ async def sfx_btn(self, interaction: discord.Interaction, button: Button):
1299
+ if interaction.user.id != self.user_id: return
1300
+ val = not self.settings.get("include_sfx", False)
1301
+ db.update_setting(self.user_id, "include_sfx", val)
1302
+ self.settings["include_sfx"] = val
1303
+ self.update_components()
1304
+ await interaction.response.edit_message(view=self)
1305
+
1306
+ @discord.ui.button(label="وضع Advance ⚡", custom_id="adv", row=2)
1307
+ async def adv_btn(self, interaction: discord.Interaction, button: Button):
1308
+ if interaction.user.id != self.user_id: return
1309
+ val = not self.settings.get("advance_mode", False)
1310
+ db.update_setting(self.user_id, "advance_mode", val)
1311
+ self.settings["advance_mode"] = val
1312
+ self.update_components()
1313
+ await interaction.response.edit_message(view=self)
1314
+
1315
+ @bot.tree.command(name="settings", description="لوحة التحكم: تفعيل وضع Advance وتخصيص العلامات")
1316
+ async def settings_cmd(interaction: discord.Interaction):
1317
+ user = db.get_user(interaction.user.id, interaction.user.name)
1318
+ embed = discord.Embed(
1319
+ title="⚙️ لوحة الإعدادات (Settings)",
1320
+ description="قم بتخصيص طريقة الاستخراج الخاصة بك:\n\n🟢 **أخضر** = مُفعل | ⚪ **رمادي** = مُعطل\n\n⚡ **Advance Mode:** الوضع الاحترافي لاستخراج فائق الدقة.",
1321
+ color=0x3498db
1322
+ )
1323
+ if interaction.user.avatar: embed.set_thumbnail(url=interaction.user.avatar.url)
1324
+ await interaction.response.send_message(embed=embed, view=SettingsView(interaction.user.id, user), ephemeral=True)
1325
+
1326
+
1327
+ # ================================
1328
+ # 📝 16. Translation & Paraphrasing Tools
1329
+ # ================================
1330
+ async def text_ai_helper(prompt_text):
1331
+ current_api_key = ai_engine.get_valid_key()
1332
+ if not current_api_key: return "❌ لا توجد مفاتيح متاحة حالياً، يرجى الانتظار."
1333
+
1334
+ genai.configure(api_key=current_api_key)
1335
+ try:
1336
+ model = genai.GenerativeModel("gemini-2.5-flash")
1337
+ response = await asyncio.wait_for(
1338
+ asyncio.to_thread(model.generate_content, prompt_text),
1339
+ timeout=15.0
1340
+ )
1341
+ return response.text.strip() if response.text else "❌ ف��لت العملية."
1342
+ except Exception as e:
1343
+ ai_engine.report_dead_key(current_api_key)
1344
+ return f"⚠️ حدث خطأ أثناء الاتصال. تم تبديل المفتاح، حاول مجدداً."
1345
+
1346
+ @bot.tree.command(name="translate", description="ترجمة احترافية للنصوص مع دعم تعدد اللغات")
1347
+ @app_commands.describe(text="النص المراد ترجمته", target_lang="اللغة الهدف (الافتراضي: العربية)", source_lang="لغة المصدر (الافتراضي: اكتشاف تلقائي)")
1348
+ async def translate_cmd(interaction: discord.Interaction, text: str, target_lang: str = "العربية", source_lang: str = "اكتشاف تلقائي"):
1349
+ await interaction.response.defer(ephemeral=False)
1350
+
1351
+ prompt = (
1352
+ f"You are a highly skilled professional translator specializing in Manga, Manhwa, and Comics.\n"
1353
+ f"Your task is to translate the following text from [{source_lang}] to [{target_lang}].\n\n"
1354
+ f"STRICT RULES:\n"
1355
+ f"1. Preserve the exact tone, emotion, and context of the scene (e.g., shouting, whispering, thoughts).\n"
1356
+ f"2. Adapt idioms, cultural references, and slang to sound completely natural and eloquent in {target_lang}.\n"
1357
+ f"3. Maintain formatting if there are any specific tags.\n"
1358
+ f"4. OUTPUT ONLY THE TRANSLATED TEXT. DO NOT add any notes, explanations, or introductions.\n\n"
1359
+ f"Original Text:\n{text}"
1360
+ )
1361
+
1362
+ result = await text_ai_helper(prompt)
1363
+
1364
+ embed = discord.Embed(title="🌐 المترجم الاحترافي", color=0x2ecc71)
1365
+ embed.add_field(name=f"النص الأصلي ({source_lang}):", value=f"
1366
+ # ================================
1367
+ # 🎫 19. Ticket & Support System
1368
+ # ================================
1369
+ class TicketControlView(discord.ui.View):
1370
+ def __init__(self):
1371
+ super().__init__(timeout=None)
1372
+
1373
+ @discord.ui.button(label="🔒 إغلاق التذكرة", style=discord.ButtonStyle.danger, custom_id="close_ticket")
1374
+ async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
1375
+ await interaction.response.send_message("⏳ جاري إغلاق التذكرة وحفظ السجل...", ephemeral=True)
1376
+ transcript = f"--- سجل تذكرة {interaction.channel.name} ---\n\n"
1377
+ async for msg in interaction.channel.history(limit=100, oldest_first=True):
1378
+ transcript += f"[{msg.created_at.strftime('%Y-%m-%d %H:%M')}] {msg.author.name}: {msg.content}\n"
1379
+
1380
+ log_file = discord.File(io.StringIO(transcript), filename=f"{interaction.channel.name}_log.txt")
1381
+ log_channel = bot.get_channel(LOG_CHANNEL_ID)
1382
+ if log_channel:
1383
+ await log_channel.send(content=f"📁 سجل تذكرة مغلقة: {interaction.channel.name}", file=log_file)
1384
+
1385
+ await asyncio.sleep(2)
1386
+ await interaction.channel.delete(reason="تم إغلاق التذكرة.")
1387
+
1388
+ class TicketSetupView(discord.ui.View):
1389
+ def __init__(self):
1390
+ super().__init__(timeout=None)
1391
+
1392
+ @discord.ui.button(label="🎫 فتح تذكرة دعم", style=discord.ButtonStyle.primary, custom_id="open_ticket", emoji="🛠️")
1393
+ async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
1394
+ guild = interaction.guild
1395
+ category = discord.utils.get(guild.categories, name="🎫 • Tickets")
1396
+ if not category:
1397
+ category = await guild.create_category("🎫 • Tickets")
1398
+
1399
+ overwrites = {
1400
+ guild.default_role: discord.PermissionOverwrite(read_messages=False),
1401
+ interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True, attach_files=True),
1402
+ guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True, manage_channels=True)
1403
+ }
1404
+
1405
+ ticket_channel = await guild.create_text_channel(f"تذكرة-{interaction.user.name}", category=category, overwrites=overwrites)
1406
+ embed = discord.Embed(title="🎫 تذكرة دعم فني", description=f"أهلاً بك {interaction.user.mention}!\nيرجى طرح مشكلتك أو طلبك بالتفصيل، وسيقوم أحد المشرفين بالرد عليك قريباً.\n\nلإغلاق التذكرة، اضغط على الزر بالأسفل.", color=0x3498db)
1407
+ await ticket_channel.send(content=f"{interaction.user.mention}", embed=embed, view=TicketControlView())
1408
+ await interaction.response.send_message(f"✅ تم فتح تذكرتك بنجاح: {ticket_channel.mention}", ephemeral=True)
1409
+
1410
+ @bot.tree.command(name="setup_tickets", description="إعداد نظام التذاكر (للمشرفين)")
1411
+ async def setup_tickets_cmd(interaction: discord.Interaction):
1412
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
1413
+ embed = discord.Embed(title="🛠️ مركز الدعم الفني والمساعدة", description="إذا واجهتك أي مشكلة في الاستخراج، أو ترغب في الانضمام للفريق، أو لديك استفسار للإدارة...\n\n**اضغط على الزر بالأسفل لفتح تذكرة خاصة بك! ⬇️**", color=0x2c3e50)
1414
+ await interaction.channel.send(embed=embed, view=TicketSetupView())
1415
+ await interaction.response.send_message("✅ تم إنشاء لوحة التذاكر.", ephemeral=True)
1416
+
1417
+
1418
+ # ================================
1419
+ # 🛡️ 20. Auto-Mod, Trivia & Engagement
1420
+ # ================================
1421
+ active_trivia = {}
1422
+ MANHWA_DB = [
1423
+ {"name": "سونغ جين وو", "hint": "بطل سولو ليفلينج (Solo Leveling)", "img": "https://i.pinimg.com/736x/1a/0a/7b/1a0a7b4f59fb59173f478f2674b967a5.jpg"},
1424
+ {"name": "كيم دوكجا", "hint": "بطل وجهة نظر القارئ الكلي العلم (ORV)", "img": "https://i.pinimg.com/736x/d6/3e/26/d63e261cefc48b26e0bdf605e55b6ef1.jpg"},
1425
+ {"name": "آرثر", "hint": "بطل البداية بعد النهاية (TBATE)", "img": "https://i.pinimg.com/736x/8e/31/76/8e3176d65b169b2d837697ab41ec85c7.jpg"},
1426
+ {"name": "تشونغ ميونغ", "hint": "بطل عودة طائفة جبل هوا", "img": "https://i.pinimg.com/736x/6f/a7/9d/6fa79da2295ed749d214a1e9c8bc8059.jpg"}
1427
+ ]
1428
+
1429
+ @bot.event
1430
+ async def on_message(message: discord.Message):
1431
+ if message.author.bot: return
1432
+
1433
+ guild_id = message.guild.id
1434
+ if guild_id in active_trivia:
1435
+ game = active_trivia[guild_id]
1436
+ if message.channel.id == game["channel_id"]:
1437
+ if game["answer"].lower() in message.content.lower():
1438
+ winner = message.author
1439
+ points = game["points"]
1440
+ db.modify_points(winner.id, points)
1441
+
1442
+ embed = discord.Embed(title="🎉 لدينا فائز!", description=f"ألف مبروك لـ {winner.mention} عبقري المانهوا!\nالإجابة الصحيحة هي: **{game['answer']}**\n\n🎁 **كسبت {points:.2f} نقطة استخراج!**", color=0x2ecc71)
1443
+ await message.channel.send(embed=embed)
1444
+ del active_trivia[guild_id]
1445
+
1446
+ if not message.author.guild_permissions.manage_messages:
1447
+ if "discord.gg/" in message.content or "discord.com/invite/" in message.content:
1448
+ await message.delete()
1449
+ try:
1450
+ await message.author.timeout(timedelta(minutes=10), reason="إرسال رابط سيرفر خارجي")
1451
+ warn_msg = await message.channel.send(f"⚠️ {message.author.mention} تم إعطاؤك Mute لمدة 10 دقائق بسبب نشر روابط خارجية!")
1452
+ await asyncio.sleep(5); await warn_msg.delete()
1453
+ except: pass
1454
+
1455
+ @bot.tree.command(name="trivia_auto", description="تشغيل لعبة خمن الشخصية تلقائياً")
1456
+ async def trivia_auto_cmd(interaction: discord.Interaction):
1457
+ if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
1458
+ guild_id = interaction.guild.id
1459
+ if guild_id in active_trivia: return await interaction.response.send_message("❌ هناك لعبة نشطة بالفعل!", ephemeral=True)
1460
+
1461
+ character = random.choice(MANHWA_DB)
1462
+ points_reward = round(random.uniform(0.1, 1.5), 2)
1463
+
1464
+ active_trivia[guild_id] = {"answer": character["name"], "points": points_reward, "channel_id": interaction.channel.id}
1465
+
1466
+ embed = discord.Embed(title="🎮 لُعْبَة خَمِّن الْمَانْهْوَا!", description=f"أول شخص يكتب اسم هذه الشخصية بشكل صحيح سيفوز بـ **{points_reward} نقطة استخراج!** 🎁", color=0xe67e22)
1467
+ embed.add_field(name="💡 تلميح:", value=character["hint"], inline=False)
1468
+ embed.set_image(url=character["img"])
1469
+ await interaction.response.send_message(embed=embed)
1470
+
1471
+ @bot.tree.command(name="embed", description="إنشاء رسالة إدارية فخمة (للمشرفين)")
1472
+ async def embed_cmd(interaction: discord.Interaction, title: str, description: str, color: str = "blue", image_url: str = None):
1473
+ if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
1474
+ color_mapping = {"red": 0xe74c3c, "blue": 0x3498db, "green": 0x2ecc71, "yellow": 0xf1c40f, "black": 0x000000}
1475
+ embed = discord.Embed(title=title, description=description.replace("\\n", "\n"), color=color_mapping.get(color.lower(), 0x3498db))
1476
+ if image_url: embed.set_image(url=image_url)
1477
+ await interaction.channel.send(embed=embed)
1478
+ await interaction.response.send_message("✅ تم إرسال الإعلان.", ephemeral=True)
1479
+
1480
+ @bot.tree.command(name="giveaway_start", description="بدء مسابقة جديدة (للمشرفين)")
1481
+ async def giveaway_start_cmd(interaction: discord.Interaction, prize: str, hours: int, winners_count: int = 1):
1482
+ if not interaction.user.guild_permissions.manage_messages: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
1483
+ end_time = discord.utils.utcnow() + timedelta(hours=hours)
1484
+ embed = discord.Embed(title="🎉 سحب جديد! (Giveaway)", description=f"**الجائزة:** {prize}\n**عدد الفائزين:** {winners_count}\n\nاضغط على الريأكشن 🎉 للمشاركة!\n\n⏳ **ينتهي في:** <t:{int(end_time.timestamp())}:R>", color=0x9b59b6)
1485
+ await interaction.response.send_message("✅ تم إطلاق السحب!", ephemeral=True)
1486
+ msg = await interaction.channel.send(embed=embed)
1487
+ await msg.add_reaction("🎉")
1488
+
1489
+ async def finish_giveaway():
1490
+ await asyncio.sleep(hours * 3600)
1491
+ new_msg = await interaction.channel.fetch_message(msg.id)
1492
+ reaction = discord.utils.get(new_msg.reactions, emoji="🎉")
1493
+ users = [user async for user in reaction.users() if not user.bot]
1494
+ if len(users) == 0: return await interaction.channel.send(f"❌ انتهى السحب ولم يشارك أحد!")
1495
+ winners = random.sample(users, min(winners_count, len(users)))
1496
+ winners_mentions = ", ".join([w.mention for w in winners])
1497
+ win_embed = discord.Embed(title="🎊 انتهى السحب!", description=f"**الجائزة:** {prize}\n**الفائزون:** {winners_mentions}", color=0x2ecc71)
1498
+ await new_msg.reply(content=f"مبروك لـ {winners_mentions}!", embed=win_embed)
1499
+
1500
+ bot.loop.create_task(finish_giveaway())
1501
+
1502
+ @bot.event
1503
+ async def on_raw_reaction_add(payload: discord.RawReactionActionEvent):
1504
+ if payload.emoji.name == "⭐":
1505
+ channel = bot.get_channel(payload.channel_id)
1506
+ msg = await channel.fetch_message(payload.message_id)
1507
+ reaction = discord.utils.get(msg.reactions, emoji="⭐")
1508
+ if reaction and reaction.count >= 5:
1509
+ starboard_channel = discord.utils.get(msg.guild.channels, name="أفضل-اللقطات")
1510
+ if not starboard_channel: return
1511
+ async for old_msg in starboard_channel.history(limit=50):
1512
+ if str(msg.id) in old_msg.content: return
1513
+ embed = discord.Embed(description=msg.content, color=0xf1c40f, timestamp=msg.created_at)
1514
+ embed.set_author(name=msg.author.display_name, icon_url=msg.author.avatar.url if msg.author.avatar else None)
1515
+ if msg.attachments: embed.set_image(url=msg.attachments[0].url)
1516
+ await starboard_channel.send(content=f"⭐ **{reaction.count}** | {channel.mention} | ID: {msg.id}", embed=embed)
1517
+
1518
+ @bot.event
1519
+ async def on_member_join(member):
1520
+ guild = member.guild
1521
+ try:
1522
+ role = guild.get_role(AUTO_ROLE_ID)
1523
+ if role: await member.add_roles(role)
1524
+ except: pass
1525
+
1526
+ welcome_channel = guild.get_channel(WELCOME_CHANNEL_ID)
1527
+ if welcome_channel:
1528
+ embed = discord.Embed(title=f"مرحباً بك في {guild.name}! 🎭⚡", description=f"أهلاً بك {member.mention}!\n\n📊 **أنت العضو رقم:** `{guild.member_count}`\n🎁 اكتب `/register` للحصول على غرفتك و 3 نقاط مجانية!", color=0x2ecc71)
1529
+ if member.avatar: embed.set_thumbnail(url=member.avatar.url)
1530
+ await welcome_channel.send(content=member.mention, embed=embed)
1531
+
1532
+ @tasks.loop(minutes=15)
1533
+ async def live_stats_updater():
1534
+ for guild in bot.guilds:
1535
+ for channel in guild.voice_channels:
1536
+ if "الأعضاء:" in channel.name or "Members:" in channel.name:
1537
+ try: await channel.edit(name=f"👥 الأعضاء: {guild.member_count}")
1538
+ except: pass
1539
+
1540
+ def get_emoji_for_name(name):
1541
+ name = name.lower()
1542
+ if any(k in name for k in ["ترحيب", "welcome"]): return "👋"
1543
+ if any(k in name for k in ["شات", "سوالف", "chat", "عام"]): return "💬"
1544
+ if any(k in name for k in ["إدارة", "admin", "مشرفين"]): return "🛡️"
1545
+ if any(k in name for k in ["تذاكر", "ticket"]): return "🎫"
1546
+ if any(k in name for k in ["قوانين", "rules"]): return "📜"
1547
+ return "📁"
1548
+
1549
+ @bot.tree.command(name="setup_server", description="تهيئة وتزيين السيرفر بالكامل (إيموجيات وتنسيق)")
1550
+ async def setup_server_cmd(interaction: discord.Interaction):
1551
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌ للمشرفين فقط.", ephemeral=True)
1552
+ await interaction.response.defer(ephemeral=True)
1553
+ guild = interaction.guild
1554
+
1555
+ if not discord.utils.get(guild.channels, name="أفضل-اللقطات"):
1556
+ await guild.create_text_channel("أفضل-اللقطات", topic="تحصل الرسائل المميزة على ⭐ لتظهر هنا!")
1557
+
1558
+ for category in guild.categories:
1559
+ if not re.match(r'^[^\w\s]\s*•', category.name):
1560
+ try: await category.edit(name=f"{get_emoji_for_name(category.name)} • {category.name}")
1561
+ except: pass
1562
+
1563
+ for channel in guild.text_channels:
1564
+ if not re.match(r'^[^\w\s]ـ', channel.name) and "-" not in channel.name[:3]:
1565
+ try: await channel.edit(name=f"{get_emoji_for_name(channel.name)}ـ{channel.name.replace(' ', '-')}")
1566
+ except: pass
1567
+
1568
+ await interaction.followup.send("✅ تم تهيئة وتزيين السيرفر بنجاح!")
1569
+
1570
+
1571
+ # ================================
1572
+ # 💰 21. Admin, Economy & Core Commands
1573
+ # ================================
1574
+ @bot.tree.command(name="add_credits", description="إضافة رصيد لمستخدم (للمشرفين)")
1575
+ async def add_cr(interaction: discord.Interaction, user: discord.User, amount: float):
1576
+ 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)
1577
+ new_points = db.modify_points(user.id, amount)
1578
+ await interaction.response.send_message(f"✅ تمت إضافة **{amount}** لـ {user.mention}. الرصيد: **{new_points:.2f}**")
1579
+
1580
+ @bot.tree.command(name="remove_credits", description="خصم رصيد من مستخدم (للمشرفين)")
1581
+ async def rem_cr(interaction: discord.Interaction, user: discord.User, amount: float):
1582
+ 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)
1583
+ new_points = db.modify_points(user.id, -amount)
1584
+ await interaction.response.send_message(f"✅ تم خصم **{amount}** من {user.mention}. الرصيد: **{new_points:.2f}**")
1585
+
1586
+ @bot.tree.command(name="add_rank", description="تغيير رتبة مستخدم (للمشرفين)")
1587
+ @app_commands.choices(rank=[app_commands.Choice(name="Normal", value="Normal"), app_commands.Choice(name="VIP", value="VIP"), app_commands.Choice(name="VVIP", value="VVIP")])
1588
+ async def add_rank(interaction: discord.Interaction, user: discord.User, rank: app_commands.Choice[str]):
1589
+ 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)
1590
+ with db.lock:
1591
+ uid = str(user.id)
1592
+ if uid in db.db:
1593
+ db.db[uid]["rank"] = rank.value
1594
+ db.save_db()
1595
+ await interaction.response.send_message(f"✅ تم تغيير رتبة {user.mention} إلى **{rank.value}**")
1596
+
1597
+ @bot.tree.command(name="add_admin", description="إضافة أدمن للبوت")
1598
+ async def add_admin_cmd(interaction: discord.Interaction, user: discord.User):
1599
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
1600
+ db.add_bot_admin(str(user.id))
1601
+ await interaction.response.send_message(f"✅ تم إضافة {user.mention} كأدمن للبوت بنجاح!")
1602
+
1603
+ @bot.tree.command(name="cleanup_rooms", description="مسح الغرف الخاصة الميتة")
1604
+ async def cleanup_rooms_cmd(interaction: discord.Interaction, days_inactive: int = 30):
1605
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
1606
+ await interaction.response.send_message("⏳ جاري التنظيف...", ephemeral=True)
1607
+ category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON")
1608
+ if not category: return await interaction.followup.send("❌ القسم غير موجود.")
1609
+ deleted, cutoff = 0, discord.utils.utcnow() - timedelta(days=days_inactive)
1610
+ for channel in category.channels:
1611
+ if isinstance(channel, discord.TextChannel):
1612
+ last_msg = None
1613
+ async for msg in channel.history(limit=1): last_msg = msg
1614
+ if last_msg is None or last_msg.created_at < cutoff:
1615
+ await channel.delete(reason="تنظيف تلقائي")
1616
+ deleted += 1
1617
+ await asyncio.sleep(1)
1618
+ await interaction.followup.send(f"🧹 تم حذف **{deleted}** غرفة ميتة.")
1619
+
1620
+ @bot.tree.command(name="broadcast", description="إرسال إعلان للغرف الخاصة")
1621
+ async def broadcast_cmd(interaction: discord.Interaction, message: str):
1622
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
1623
+ await interaction.response.send_message("⏳ جاري الإرسال...", ephemeral=True)
1624
+ category = discord.utils.get(interaction.guild.categories, name="🎭 • MA SON")
1625
+ count, embed = 0, discord.Embed(title="📢 إعلان إداري", description=message, color=0xe74c3c)
1626
+ for channel in category.channels:
1627
+ try:
1628
+ await channel.send(embed=embed)
1629
+ count += 1
1630
+ await asyncio.sleep(0.5)
1631
+ except: pass
1632
+ await interaction.followup.send(f"✅ تم الإرسال إلى {count} غرفة.")
1633
+
1634
+ @bot.tree.command(name="lock", description="قفل الروم")
1635
+ async def lock_room(interaction: discord.Interaction):
1636
+ if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True)
1637
+ ow = interaction.channel.overwrites_for(interaction.guild.default_role)
1638
+ ow.send_messages = False
1639
+ await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow)
1640
+ await interaction.response.send_message("🔒 تم قفل الغرفة!")
1641
+
1642
+ @bot.tree.command(name="unlock", description="فتح الروم")
1643
+ async def unlock_room(interaction: discord.Interaction):
1644
+ if not interaction.user.guild_permissions.manage_channels: return await interaction.response.send_message("❌", ephemeral=True)
1645
+ ow = interaction.channel.overwrites_for(interaction.guild.default_role)
1646
+ ow.send_messages = True
1647
+ await interaction.channel.set_permissions(interaction.guild.default_role, overwrite=ow)
1648
+ await interaction.response.send_message("🔓 تم فتح الغرفة!")
1649
+
1650
+ 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"]]
1651
+ @bot.tree.command(name="set_models", description="تغيير موديل الذكاء الاصطناعي")
1652
+ @app_commands.choices(normal_model=MODELS_LIST, advance_model=MODELS_LIST)
1653
+ async def set_models_cmd(interaction: discord.Interaction, normal_model: app_commands.Choice[str], advance_model: app_commands.Choice[str]):
1654
+ if not interaction.user.guild_permissions.administrator: return await interaction.response.send_message("❌", ephemeral=True)
1655
+ db.update_bot_setting("normal_model", normal_model.value)
1656
+ db.update_bot_setting("advance_model", advance_model.value)
1657
+ await interaction.response.send_message("✅ تم تحديث الموديلات.", ephemeral=True)
1658
+
1659
+ @bot.tree.command(name="add_emoji", description="إضافة إيموجي للسيرفر")
1660
+ async def add_emoji_cmd(interaction: discord.Interaction, name: str, link: str = None, image: discord.Attachment = None):
1661
+ if not interaction.user.guild_permissions.manage_emojis: return await interaction.response.send_message("❌", ephemeral=True)
1662
+ await interaction.response.defer(ephemeral=True)
1663
+ img_data = await image.read() if image else requests.get(link).content if link else None
1664
+ if img_data:
1665
+ emoji = await interaction.guild.create_custom_emoji(name=name, image=img_data)
1666
+ await interaction.followup.send(f"✅ تم: {emoji}")
1667
+
1668
+ @bot.tree.command(name="status", description="مراقبة حالة عمليات الاستخراج والدمج")
1669
+ async def status_cmd(interaction: discord.Interaction):
1670
+ total = len(bot_queue["ocr"]) + len(bot_queue["merge"])
1671
+ embed = discord.Embed(title="🚦 حالة البوت", description=f"يوجد **{total}** عمليات قيد التنفيذ." if total else "🟢 البوت خامل حالياً.", color=0x3498db)
1672
+ await interaction.response.send_message(embed=embed)
1673
+
1674
+ @bot.tree.command(name="profile", description="عرض إحصائياتك ورصيدك")
1675
+ async def profile_cmd(interaction: discord.Interaction, target_user: discord.User = None):
1676
+ t = target_user or interaction.user
1677
+ d = db.get_user(t.id, t.name)
1678
+ embed = discord.Embed(title=f"👤 الملف الشخصي: {t.name}", color=0xf1c40f)
1679
+ embed.add_field(name="💰 الرصيد", value=f"**{d['points']:.2f}**", inline=True)
1680
+ embed.add_field(name="🏆 الرتبة", value=f"**{d.get('rank','Normal')}**", inline=True)
1681
+ embed.add_field(name="🖼️ صور مستخرجة", value=f"**{d.get('images_extracted', 0)}**", inline=True)
1682
+ if t.avatar: embed.set_thumbnail(url=t.avatar.url)
1683
+ await interaction.response.send_message(embed=embed)
1684
+
1685
+ @bot.tree.command(name="top", description="لوحة الشرف لأفضل المترجمين")
1686
+ async def top_cmd(interaction: discord.Interaction):
1687
+ 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]
1688
+ embed = discord.Embed(title="🏆 لوحة الشرف (Top 5)", color=0xf1c40f)
1689
+ for i, u in enumerate(users): embed.add_field(name=f"{['🥇','🥈','🥉','🏅','🏅'][i]} {u.get('name','مجهول')}", value=f"استخرج: {u.get('images_extracted',0)}", inline=False)
1690
+ await interaction.response.send_message(embed=embed)
1691
+
1692
+ @bot.tree.command(name="register", description="إنشاء حسابك واستلام 3 نقاط هدية")
1693
+ async def register_cmd(interaction: discord.Interaction):
1694
+ user = db.get_user(interaction.user.id, interaction.user.name)
1695
+ if user.get("registered"): return await interaction.response.send_message("❌ أنت مسجل بالفعل!", ephemeral=True)
1696
+ db.modify_points(interaction.user.id, 3.0)
1697
+ with db.lock:
1698
+ db.db[str(interaction.user.id)]["registered"] = True
1699
+ db.save_db()
1700
+
1701
+ guild = interaction.guild
1702
+ category = discord.utils.get(guild.categories, name="🎭 • MA SON") or await guild.create_category("🎭 • MA SON")
1703
+ overwrites = {guild.default_role: discord.PermissionOverwrite(read_messages=False), interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True)}
1704
+ ch = await guild.create_text_channel(f"مكتب-{interaction.user.name}", category=category, overwrites=overwrites)
1705
+ await ch.send(f"مرحباً {interaction.user.mention} 🎁 لقد حصلت على 3 نقاط مجانية!")
1706
+ await interaction.response.send_message(f"✅ تم إنشاء غرفتك: {ch.mention}", ephemeral=True)
1707
+
1708
+ @bot.tree.command(name="info", description="معلومات البوت والمسؤولين")
1709
+ async def info_cmd(interaction: discord.Interaction):
1710
+ embed = discord.Embed(title="ℹ️ معلومات البوت", color=0x2c3e50)
1711
+ embed.add_field(name="👑 المالك", value=interaction.guild.owner.mention, inline=True)
1712
+ if interaction.user.guild_permissions.administrator:
1713
+ 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)
1714
+ await interaction.response.send_message(embed=embed, ephemeral=True)
1715
+
1716
+ @bot.tree.command(name="help", description="دليل أوامر البوت للأعضاء")
1717
+ async def help_cmd(interaction: discord.Interaction):
1718
+ embed = discord.Embed(title="📚 دليل أوامر البوت", color=0x2ecc71)
1719
+ embed.add_field(name="☁️ الاستخراج والدمج", value="**`/ocr`**: استخراج النصوص.\n**`/ocrweb`**: استخراج من الروابط.\n**`/merge`**: دمج صور الفصول.\n**`/settings`**: لوحة تحكم الاستخراج.", inline=False)
1720
+ embed.add_field(name="🌍 الترجمة والمساعدة", value="**`/translate`**: ترجمة المانهوا.\n**`/rewrite`**: إعادة صياغة بأسلوب درامي أو كوميدي.", inline=False)
1721
+ embed.add_field(name="👤 حسابك", value="**`/register`**: إنشاء الغرفة واستلام الهدية.\n**`/profile`**: عرض رصيدك.\n**`/top`**: أفضل المترجمين.", inline=False)
1722
+ await interaction.response.send_message(embed=embed, ephemeral=True)
1723
+
1724
+ # ================================
1725
+ # 🚀 22. Bot Startup & Hugging Face Keep-Alive
1726
+ # ================================
1727
+ @bot.event
1728
+ async def on_ready():
1729
+ print("⏳ جاري البحث عن نسخ احتياطية للبيانات في ديسكورد...")
1730
+ try:
1731
+ log_channel = bot.get_channel(LOG_CHANNEL_ID)
1732
+ if log_channel:
1733
+ async for msg in log_channel.history(limit=50):
1734
+ if msg.author == bot.user and msg.attachments:
1735
+ found_files = 0
1736
+ for att in msg.attachments:
1737
+ if att.filename == "users_db.json":
1738
+ await att.save(USERS_DB_FILE)
1739
+ db.db = db.load_db()
1740
+ found_files += 1
1741
+ elif att.filename == "tracker_cache.json":
1742
+ await att.save(TRACKER_DB_FILE)
1743
+ found_files += 1
1744
+ if found_files > 0:
1745
+ print("✅ تم استرجاع البيانات بنجاح من ديسكورد!")
1746
+ break
1747
+ except Exception as e:
1748
+ print(f"⚠️ خطأ في الاسترجاع: {e}")
1749
+
1750
+ if not backup_task.is_running(): backup_task.start()
1751
+ if not live_stats_updater.is_running(): live_stats_updater.start()
1752
+ if not advanced_tracker_loop.is_running(): advanced_tracker_loop.start()
1753
+
1754
+ try:
1755
+ await bot.tree.sync()
1756
+ print("🔄 Synced Commands Successfully")
1757
+ except Exception as e: print(f"❌ Failed to sync: {e}")
1758
+
1759
+ print(f'🔥 Logged in as {bot.user} (ID: {bot.user.id})')
1760
+ await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name="Over Webtoons | /help"))
1761
+
1762
+ # تشغيل الخادم الوهمي الخاص بـ Hugging Face لضمان بقاء البوت أونلاين 24/7
1763
+ keep_alive()
1764
+
1765
+ if __name__ == "__main__":
1766
+ bot.run(DISCORD_TOKEN)