Da7e commited on
Commit
f7fab67
·
verified ·
1 Parent(s): 40d4464

Upload 17 files

Browse files
Files changed (3) hide show
  1. app.py +9 -3
  2. telegram_bot.py +504 -0
  3. update_server.py +1859 -0
app.py CHANGED
@@ -6,10 +6,12 @@ import os
6
  import sys
7
  from pathlib import Path
8
 
9
- # Ensure root and server directory are on Python path
10
  BASE_DIR = Path(__file__).resolve().parent
 
 
 
 
11
  sys.path.insert(0, str(BASE_DIR))
12
- sys.path.insert(0, str(BASE_DIR / "server"))
13
 
14
  # Hugging Face Spaces listens on port 7860 by default
15
  if not os.environ.get("PORT"):
@@ -17,7 +19,11 @@ if not os.environ.get("PORT"):
17
 
18
  os.environ["HUGGINGFACE"] = "true"
19
 
20
- import update_server
 
 
 
 
21
 
22
  if __name__ == "__main__":
23
  print("=" * 70)
 
6
  import sys
7
  from pathlib import Path
8
 
 
9
  BASE_DIR = Path(__file__).resolve().parent
10
+
11
+ # Check both server/ directory and root directory
12
+ if (BASE_DIR / "server").exists():
13
+ sys.path.insert(0, str(BASE_DIR / "server"))
14
  sys.path.insert(0, str(BASE_DIR))
 
15
 
16
  # Hugging Face Spaces listens on port 7860 by default
17
  if not os.environ.get("PORT"):
 
19
 
20
  os.environ["HUGGINGFACE"] = "true"
21
 
22
+ try:
23
+ import update_server
24
+ except ImportError:
25
+ # Try importing from server folder explicitly
26
+ from server import update_server
27
 
28
  if __name__ == "__main__":
29
  print("=" * 70)
telegram_bot.py ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Ensure UTF-8 console output on Windows to prevent UnicodeEncodeError
5
+ try:
6
+ if hasattr(sys.stdout, "reconfigure"):
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ if hasattr(sys.stderr, "reconfigure"):
9
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
10
+ except Exception:
11
+ pass
12
+
13
+ import json
14
+ import time
15
+ import socket
16
+ import urllib.request
17
+ import urllib.parse
18
+ import threading
19
+ from pathlib import Path
20
+
21
+ # Telegram Bot Configuration - Enforce User's Active Bot Token
22
+ ACTIVE_BOT_TOKEN = "8993921362:AAFglRSgWPIiNZOLPEh8JSgBrW8RWPfaQ9c"
23
+ env_tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
24
+ if env_tok and env_tok.startswith("8993921362"):
25
+ TELEGRAM_TOKEN = env_tok
26
+ else:
27
+ TELEGRAM_TOKEN = ACTIVE_BOT_TOKEN
28
+
29
+ BASE_DIR = Path(__file__).resolve().parent
30
+ ADMIN_CHATS_FILE = BASE_DIR / "admin_chats.json"
31
+
32
+ # Default Commander ID
33
+ _admin_chat_ids: set[int] = {6322019827}
34
+
35
+ def load_admin_chats():
36
+ global _admin_chat_ids
37
+ if ADMIN_CHATS_FILE.exists():
38
+ try:
39
+ with open(ADMIN_CHATS_FILE, "r", encoding="utf-8") as f:
40
+ data = json.load(f)
41
+ _admin_chat_ids.update(data.get("admin_chats", []))
42
+ except Exception:
43
+ pass
44
+
45
+ def save_admin_chats():
46
+ try:
47
+ with open(ADMIN_CHATS_FILE, "w", encoding="utf-8") as f:
48
+ json.dump({"admin_chats": list(_admin_chat_ids)}, f, indent=2)
49
+ except Exception:
50
+ pass
51
+
52
+ load_admin_chats()
53
+
54
+ def send_chat_action(chat_id: int | str, action: str = "typing") -> None:
55
+ """Sends typing status indicator immediately so user knows bot is replying."""
56
+ if not TELEGRAM_TOKEN:
57
+ return
58
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendChatAction"
59
+ try:
60
+ body = json.dumps({"chat_id": chat_id, "action": action}).encode("utf-8")
61
+ req = urllib.request.Request(
62
+ url,
63
+ data=body,
64
+ headers={"Content-Type": "application/json", "User-Agent": "TitanStrikeBot/2.0"}
65
+ )
66
+ urllib.request.urlopen(req, timeout=3)
67
+ except Exception:
68
+ pass
69
+
70
+ def send_telegram_message(chat_id: int | str, text: str, reply_markup: dict = None) -> bool:
71
+ """Sends a Telegram message using the HTTP API ultra-fast."""
72
+ if not TELEGRAM_TOKEN:
73
+ return False
74
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
75
+ payload = {
76
+ "chat_id": chat_id,
77
+ "text": text,
78
+ "parse_mode": "HTML"
79
+ }
80
+ if reply_markup:
81
+ payload["reply_markup"] = reply_markup
82
+
83
+ try:
84
+ body = json.dumps(payload).encode("utf-8")
85
+ req = urllib.request.Request(
86
+ url,
87
+ data=body,
88
+ headers={"Content-Type": "application/json", "User-Agent": "TitanStrikeBot/2.0"}
89
+ )
90
+ with urllib.request.urlopen(req, timeout=8) as resp:
91
+ ok = (resp.status == 200)
92
+ if ok:
93
+ safe_text = str(text)[:50].replace("\n", " ")
94
+ print(f"[TelegramBot] 📤 تم إرسال الرد لـ {chat_id}: {safe_text}")
95
+ return ok
96
+ except Exception as e:
97
+ # Fallback to plain text if HTML parsing failed
98
+ try:
99
+ payload.pop("parse_mode", None)
100
+ body = json.dumps(payload).encode("utf-8")
101
+ req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"})
102
+ with urllib.request.urlopen(req, timeout=8) as resp:
103
+ return resp.status == 200
104
+ except Exception:
105
+ print(f"[TelegramBot] ⚠️ فشل إرسال الرسالة إلى {chat_id}: {e}")
106
+ return False
107
+
108
+ def send_telegram_photo(chat_id: int | str, photo_path: str, caption: str = "") -> bool:
109
+ """Sends a local photo file directly to Telegram chat via multipart/form-data."""
110
+ if not TELEGRAM_TOKEN or not os.path.exists(photo_path):
111
+ return False
112
+ import uuid
113
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendPhoto"
114
+ boundary = f"----WebKitFormBoundary{uuid.uuid4().hex}"
115
+
116
+ with open(photo_path, "rb") as f:
117
+ file_bytes = f.read()
118
+
119
+ filename = os.path.basename(photo_path)
120
+
121
+ body = bytearray()
122
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
123
+ body.extend(f'Content-Disposition: form-data; name="chat_id"\r\n\r\n{chat_id}\r\n'.encode("utf-8"))
124
+
125
+ if caption:
126
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
127
+ body.extend(f'Content-Disposition: form-data; name="caption"\r\n\r\n{caption}\r\n'.encode("utf-8"))
128
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
129
+ body.extend(f'Content-Disposition: form-data; name="parse_mode"\r\n\r\nHTML\r\n'.encode("utf-8"))
130
+
131
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
132
+ body.extend(f'Content-Disposition: form-data; name="photo"; filename="{filename}"\r\n'.encode("utf-8"))
133
+ body.extend(b"Content-Type: image/jpeg\r\n\r\n")
134
+ body.extend(file_bytes)
135
+ body.extend(b"\r\n")
136
+ body.extend(f"--{boundary}--\r\n".encode("utf-8"))
137
+
138
+ try:
139
+ req = urllib.request.Request(
140
+ url,
141
+ data=bytes(body),
142
+ headers={"Content-Type": f"multipart/form-data; boundary={boundary}", "User-Agent": "TitanStrikeBot/2.0"}
143
+ )
144
+ with urllib.request.urlopen(req, timeout=20) as resp:
145
+ return resp.status == 200
146
+ except Exception as e:
147
+ print(f"[TelegramBot] ⚠️ فشل إرسال الصورة: {e}")
148
+ return False
149
+
150
+ def broadcast_to_admins(text: str) -> None:
151
+ """Broadcasts an urgent notification to all registered commanders."""
152
+ for chat_id in list(_admin_chat_ids):
153
+ send_telegram_message(chat_id, text)
154
+
155
+ def notify_server_error(source: str, error_msg: str) -> None:
156
+ """Sends an instant alert to the commander whenever an error occurs on the server."""
157
+ if not _admin_chat_ids:
158
+ return
159
+ now_str = time.strftime("%H:%M:%S")
160
+ alert_text = (
161
+ f"🚨 <b>[تنبيه عاجل من السيرفر - خطأ!]</b>\n"
162
+ f"⏰ <b>الوقت:</b> <code>{now_str}</code>\n"
163
+ f"📍 <b>المصدر:</b> <code>{source}</code>\n"
164
+ f"⚠️ <b>تفاصيل الخطأ:</b>\n"
165
+ f"<pre>{error_msg[:1200]}</pre>\n\n"
166
+ f"<i>💡 أرسل /status لفحص حالة السيرفر الآن.</i>"
167
+ )
168
+ broadcast_to_admins(alert_text)
169
+
170
+ def get_server_status_summary() -> str:
171
+ """Builds a comprehensive live summary of the server in milliseconds."""
172
+ try:
173
+ from update_server import (
174
+ ROOMS, ACTIVE_SESSIONS, GLOBAL_EMERGENCY_ALARM, GLOBAL_BROADCAST,
175
+ load_game_config, get_lan_ip, get_wan_ip, PORT, GUEST_PORT
176
+ )
177
+ cfg = load_game_config()
178
+ st = cfg.get("server_status", "OPEN").upper()
179
+ status_emoji = "🟢 مفتوح ومتاح (ONLINE)" if st == "OPEN" else "🔴 مغلق للصيانة (CLOSED)"
180
+
181
+ active_rooms_cnt = len(ROOMS)
182
+ active_sess_cnt = len(ACTIVE_SESSIONS)
183
+ alarm_active = GLOBAL_EMERGENCY_ALARM.get("active", False)
184
+ alarm_txt = "🚨 شغال الآن!" if alarm_active else "✅ هادئ (متوقف)"
185
+
186
+ now_str = time.strftime("%Y-%m-%d %H:%M:%S")
187
+
188
+ sys_info = ""
189
+ try:
190
+ import psutil
191
+ cpu = psutil.cpu_percent(interval=None)
192
+ mem = psutil.virtual_memory().percent
193
+ sys_info = f"💻 <b>المعالج:</b> {cpu}% | 🧠 <b>الرام:</b> {mem}%\n"
194
+ except Exception:
195
+ pass
196
+
197
+ # Player list details
198
+ players_details = []
199
+ for s_id, s in list(ACTIVE_SESSIONS.items()):
200
+ p_name = s.get("player", "لاعب")
201
+ p_mode = s.get("mode", "متصل باللعبة")
202
+ p_hero = s.get("hero", "بطل")
203
+ players_details.append(f" • 👤 <b>{p_name}</b> ({p_hero}) - <i>{p_mode}</i>")
204
+
205
+ for code, r in list(ROOMS.items()):
206
+ for pl in r.get("players_list", []):
207
+ if not any(pl in d for d in players_details):
208
+ players_details.append(f" • 👤 <b>{pl}</b> - <i>غرفة #{code}</i>")
209
+
210
+ total_online = max(active_sess_cnt, len(players_details))
211
+ players_section = f"👥 <b>اللاعبين المتصلين:</b> <code>{total_online}</code>\n"
212
+ if players_details:
213
+ players_section += "\n".join(players_details[:6]) + "\n"
214
+ else:
215
+ players_section += " <i>(لا يوجد لاعبين متصلين حالياً)</i>\n"
216
+
217
+ rooms_summary = ""
218
+ if ROOMS:
219
+ rooms_summary = "\n<b>🎮 الغرف النشطة الحالية:</b>\n"
220
+ for code, r in list(ROOMS.items())[:5]:
221
+ p_cnt = len(r.get("players_list", []))
222
+ host = r.get("host", "لاعب")
223
+ mode = r.get("mode", "مبارزة")
224
+ rooms_summary += f" • <b>#{code}</b> | {mode} | الهوست: {host} ({p_cnt} لاعبين)\n"
225
+ else:
226
+ rooms_summary = "\n🎮 <b>الغرف النشطة:</b> لا توجد غرف مفتوحة حالياً.\n"
227
+
228
+ summary = (
229
+ f"⚡ <b>[تقرير حالة سيرفر Titan Strike 3D]</b>\n"
230
+ f"📅 <b>التوقيت:</b> <code>{now_str}</code>\n\n"
231
+ f"📡 <b>حالة السيرفر:</b> {status_emoji}\n"
232
+ f"{players_section}"
233
+ f"🚪 <b>الغرف المفتوحة:</b> <code>{active_rooms_cnt}</code>\n"
234
+ f"🚨 <b>صفارات الإنذار:</b> {alarm_txt}\n"
235
+ f"{sys_info}"
236
+ f"🌐 <b>IP المحلي:</b> <code>{get_lan_ip()}</code>\n"
237
+ f"🌍 <b>IP الخارجي:</b> <code>{get_wan_ip()}</code>\n"
238
+ f"🔌 <b>البورت:</b> <code>{PORT}</code>\n"
239
+ f"{rooms_summary}\n"
240
+ f"<i>💡 أجبني أو اطلب مني أي تعديل وسأنفذه فوراً!</i>"
241
+ )
242
+ return summary
243
+ except Exception as e:
244
+ return f"⚠️ تعذر جلب ملخص السيرفر: {e}"
245
+
246
+ def execute_code_or_file_action(prompt: str) -> tuple[bool, str]:
247
+ """Restricts AI strictly to answering questions and in-game actions only (no code/file modifications)."""
248
+ p = prompt.strip().lower()
249
+
250
+ # 1. Reject any code/system file modifications
251
+ if any(k in p for k in ["عدل الكود", "تعديل الكود", "غير الكود", "احذف كود", "شيل كود", "احذف ملف", "مسح ملف"]):
252
+ return True, "🛡️ تم حظر تعديل الأكواد والملفات بأمرك يا قائدنا! الذكاء الاصطناعي مخصص فقط للرد على أسئلتك وتنفيذ الأوامر داخل اللعبة (إنشاء غرف، إعلانات، إنذار، فحص السيرفر)."
253
+
254
+ # 2. In-game room closing (safe in-game action)
255
+ if any(k in p for k in ["احذف", "حذف", "امسح", "مسح", "اقفل"]) and any(r in p for r in ["روم", "غرفة", "غرفه", "الرومات", "الغرف"]):
256
+ from update_server import ROOMS, handle_rooms_terminate
257
+ count = len(ROOMS)
258
+ for code in list(ROOMS.keys()):
259
+ handle_rooms_terminate({"room_code": code})
260
+ return True, f"🗑️ تم قفل وحذف جميع الغرف داخل اللعبة ({count} غرفة) بنجاح يا قائدنا!"
261
+
262
+ return False, ""
263
+
264
+ def handle_user_command(chat_id: int, text: str) -> bool:
265
+ """Executes special commands or queries in < 0.1 second."""
266
+ cmd = text.strip()
267
+ cmd_lower = cmd.lower()
268
+
269
+ # 1. Start Command
270
+ if cmd_lower.startswith("/start"):
271
+ _admin_chat_ids.add(chat_id)
272
+ save_admin_chats()
273
+ welcome = (
274
+ "👑 <b>أهلاً بك يا قائدنا سيف في بوت Titan Strike 3D الذكي!</b>\n\n"
275
+ "🤖 أنا <b>CyberAI</b> مساعدك التنفيذي المباشر، شغال معاك 24 ساعة بدون توقف!\n"
276
+ "⚡ أرد على كل أسئلتك، وأنَفّذ أوامرك داخل اللعبة فوراً!\n\n"
277
+ "<b>الأوامر السريعة:</b>\n"
278
+ "/status - فحص حالة السيرفر واللاعبين والموارد فوري\n"
279
+ "/rooms - عرض الغرف النشطة في اللعبة\n"
280
+ "/1v1 - إنشاء وتجهيز غرفة مبارزة 1v1 فوراً\n"
281
+ "/alarm on - تشغيل صفارات الإنذار داخل اللعبة\n"
282
+ "/alarm off - إيقاف صفارات الإنذار\n"
283
+ "/announce [رسالة] - بث إعلان بمنتصف شاشات اللاعبين\n"
284
+ "/help - قائمة الأوامر\n\n"
285
+ "<i>💬 كلمني في أي وقت بالعامية وسأجيبك فوراً كقائد وزعيم!</i>"
286
+ )
287
+ send_telegram_message(chat_id, welcome)
288
+ return True
289
+
290
+ # 2. Greetings & Check-ins ("عامل ايه", "الو", "ازيك", etc.)
291
+ if any(k in cmd_lower for k in ["صورتك", "صورتك ايه", "ابعت صورتك", "هات صورتك", "صورة البوت", "صورتك يا بوت"]):
292
+ avatar_path = BASE_DIR / "public" / "cyberai_avatar.jpg"
293
+ if avatar_path.exists():
294
+ send_telegram_photo(chat_id, str(avatar_path), "👑 <b>أنا CyberAI</b> - المساعد الذكي لقائد سيرفر Titan Strike 3D! ⚡")
295
+ return True
296
+
297
+ greetings = ["عامل ايه", "عامل ايخ", "عامل إيه", "الو", "ألو", "هلا", "مرحبا", "سلام عليكم", "صباح الخير", "مساء الخير", "انت هنا", "فينك"]
298
+ if any(cmd_lower == g or cmd_lower.startswith(g) for g in greetings):
299
+ reply = (
300
+ "👑 <b>الحمد لله يا قائدنا! أنا هنا وشغال معاك 24 ساعة ومتابع السيرفر خطوة بخطوة!</b> ⚡\n\n"
301
+ "السيرفر شغال وتمام التمام، وأنا مستعد لتنفيذ أي أمر تطلبه: تعديل كود، فحص السيرفر، أو تحدي مبارزة 1v1!\n"
302
+ "<i>طلبك إيه يا زعيم؟</i>"
303
+ )
304
+ send_telegram_message(chat_id, reply)
305
+ return True
306
+
307
+ # 3. Status variations (/status, /statues, /state, etc.)
308
+ status_variations = [
309
+ "/status", "/statues", "/state", "/statu", "/stats",
310
+ "status", "statues", "state", "stats",
311
+ "حالة السيرفر", "حاله السيرفر", "السيرفر شغال؟", "السيرفر شغال", "اخبار السيرفر", "أخبار السيرفر",
312
+ "مين بيلعب", "مين متصل", "اللاعبين", "كم لاعب"
313
+ ]
314
+ if cmd_lower in status_variations or any(cmd_lower == s for s in status_variations) or ("حالة" in cmd_lower and "سيرفر" in cmd_lower):
315
+ send_telegram_message(chat_id, get_server_status_summary())
316
+ return True
317
+
318
+ # 4. Rooms list query (only when inquiring about rooms)
319
+ if cmd_lower in ["/rooms", "/room", "الغرف", "الرومات", "الغرف النشطة", "rooms"] or (cmd_lower.startswith("عرض") and "غرف" in cmd_lower):
320
+ from update_server import ROOMS
321
+ if not ROOMS:
322
+ send_telegram_message(chat_id, "🎮 <b>لا توجد غرف نشطة حالياً بالسيرفر.</b>\nيمكنك إنشاء غرفة مبارزة فوراً بكتابة:\n<i>اعمل لي الغرف</i> أو <i>/1v1</i>")
323
+ else:
324
+ txt = f"🎮 <b>الغرف النشطة بالسيرفر ({len(ROOMS)}):</b>\n\n"
325
+ for code, r in ROOMS.items():
326
+ txt += f"🔑 <b>كود الغرفة:</b> <code>#{code}</code>\n"
327
+ txt += f"👑 <b>المضيف:</b> {r.get('host', 'لاعب')}\n"
328
+ txt += f"⚔️ <b>الطور:</b> {r.get('mode', 'مبارزة')}\n"
329
+ txt += f"👥 <b>اللاعبين:</b> {r.get('players', 1)} ({', '.join(r.get('players_list', []))})\n\n"
330
+ send_telegram_message(chat_id, txt)
331
+ return True
332
+
333
+ # 5. Room Creation & 1v1 Duel against CyberAI (instant < 0.1s execution)
334
+ is_room_create = False
335
+ if cmd_lower.startswith("/1v1") or cmd_lower.startswith("/create") or cmd_lower.startswith("/createroom"):
336
+ is_room_create = True
337
+ else:
338
+ create_verbs = ["اعمل", "سوي", "انشئ", "أنشئ", "افتح", "كريت", "جهز", "ادخل", "خش", "نلعب", "العب", "هات", "طلع"]
339
+ room_nouns = ["غرفة", "غرفه", "غرف", "الغرف", "الغرفه", "الغرفة", "روم", "رومات", "الروم", "الرومات", "1v1", "تحدي", "مبارزة", "مبارزه"]
340
+ if any(v in cmd_lower for v in create_verbs) and any(n in cmd_lower for n in room_nouns):
341
+ is_room_create = True
342
+ elif any(phrase in cmd_lower for phrase in [
343
+ "اعمل لي الغرف", "اعمل الغرف", "سوي الغرف", "اعمل روم", "اعمل لي روم", "اعمل غرفه", "اعمل غرفة",
344
+ "سوي روم", "سوي غرفة", "سوي غرفه", "انشئ روم", "افتح روم", "افتح غرفه", "افتح غرفة",
345
+ "خش اللعبة", "خش العبه", "ادخل اللعبة", "ادخل العبه", "العب معاك", "العب معايا", "نلعب سوا", "نلعب مع بعض"
346
+ ]):
347
+ is_room_create = True
348
+
349
+ if is_room_create:
350
+ try:
351
+ from update_server import execute_server_action
352
+ ok, msg, _ = execute_server_action("create_ai_room", {"hero": "Spider Hero"}, context_type="admin_dashboard")
353
+ reply = (
354
+ "⚔️ <b>سمعاً وطاعة يا قائدنا سيف! تم إنشاء وتجهيز الغرفة بنجاح!</b>\n\n"
355
+ "🔑 <b>كود الغرفة:</b> <code>#AI1V1</code>\n"
356
+ "🎮 تم بث إشعار التحدي فوراً في منتصف شاشات اللعبة بالكمبيوتر والموبايل!\n"
357
+ "ادخل الساحة وستجد CyberAI بانتظارك فوراً يا زعيم! ⚡"
358
+ )
359
+ send_telegram_message(chat_id, reply)
360
+ return True
361
+ except Exception as e:
362
+ send_telegram_message(chat_id, f"❌ حدث خطأ أثناء إنشاء الغرفة: {e}")
363
+ return True
364
+
365
+ # 6. Direct Code / File / Config modification
366
+ executed, exec_msg = execute_code_or_file_action(text)
367
+ if executed:
368
+ send_telegram_message(chat_id, f"👑 <b>سمعاً وطاعة يا قائدنا!</b>\n\n{exec_msg}")
369
+ return True
370
+
371
+ # 7. Emergency Alarm
372
+ if cmd_lower.startswith("/alarm") or "صفارة" in cmd_lower or "إنذار" in cmd_lower:
373
+ active = not any(k in cmd_lower for k in ["off", "stop", "اطفي", "وقف", "0", "إلغاء"])
374
+ try:
375
+ from update_server import execute_server_action
376
+ ok, msg, _ = execute_server_action("emergency_alarm", {"active": active}, context_type="admin_dashboard")
377
+ status_t = "تشغيل 🚨" if active else "إيقاف ✅"
378
+ send_telegram_message(chat_id, f"تم {status_t} صفارات الإنذار في اللعبة بنجاح يا قائدنا!")
379
+ return True
380
+ except Exception as e:
381
+ send_telegram_message(chat_id, f"❌ خطأ: {e}")
382
+ return True
383
+
384
+ # 8. Broadcast announcement
385
+ if cmd_lower.startswith("/announce") or cmd_lower.startswith("/broadcast"):
386
+ parts = cmd.split(maxsplit=1)
387
+ if len(parts) < 2 or not parts[1].strip():
388
+ send_telegram_message(chat_id, "⚠️ يرجى كت��بة نص الإعلان بعد الأمر، مثال:\n<code>/announce مرحباً بالجميع في السيرفر!</code>")
389
+ return True
390
+ msg_text = parts[1].strip()
391
+ try:
392
+ from update_server import execute_server_action
393
+ ok, res_msg, _ = execute_server_action("broadcast_message", {"message": msg_text}, context_type="admin_dashboard")
394
+ send_telegram_message(chat_id, f"📢 <b>تم إرسال إعلانك في منتصف شاشات جميع اللاعبين فوراً!</b>\nالرسالة: <i>{msg_text}</i>")
395
+ return True
396
+ except Exception as e:
397
+ send_telegram_message(chat_id, f"❌ خطأ: {e}")
398
+ return True
399
+
400
+ # 9. Help
401
+ if cmd_lower in ["/help", "مساعدة", "اوامر", "الأوامر", "help"]:
402
+ help_msg = (
403
+ "🛠️ <b>أوامر إدارة سيرفر Titan Strike 3D السريعة:</b>\n\n"
404
+ "/status - فحص حالة السيرفر، اللاعبين، واستهلاك الموارد\n"
405
+ "/rooms - عرض جميع الغرف النشطة في اللعبة\n"
406
+ "/1v1 - إنشاء غرفة مبارزة 1v1 ضد الذكاء الاصطناعي\n"
407
+ "/alarm on - تشغيل صفارة الإنذار العامة باللعبة\n"
408
+ "/alarm off - إيقاف صفارة الإنذار\n"
409
+ "/announce [رسالة] - بث كلمة أو إعلان في منتصف شاشات اللاعبين\n\n"
410
+ "💡 <b>المهام المخصصة:</b>\n"
411
+ "1. الإجابة على أي سؤال أو استفسار عن اللعبة والسيرفر والأبطال\n"
412
+ "2. تنفيذ أوامر اللعبة فوراً (إنشاء غرف 1v1، إعلانات، إنذار، فحص السيرفر)"
413
+ )
414
+ send_telegram_message(chat_id, help_msg)
415
+ return True
416
+
417
+ return False
418
+
419
+ def handle_gemini_chat_message(chat_id: int, user_text: str) -> None:
420
+ """Passes natural conversation to Gemini AI with sub-second response."""
421
+ from update_server import call_gemini_ai
422
+ try:
423
+ send_chat_action(chat_id, "typing")
424
+ status_code, ai_res = call_gemini_ai(user_text, context_type="admin_dashboard")
425
+ if ai_res.get("success") and ai_res.get("reply"):
426
+ reply = ai_res["reply"]
427
+ act_res = ai_res.get("action_result", "")
428
+ if act_res:
429
+ reply += f"\n\n⚡ <b>تم تنفيذ الأمر:</b> <code>{act_res}</code>"
430
+ send_telegram_message(chat_id, reply)
431
+ else:
432
+ send_telegram_message(chat_id, "أمرك يا قائدنا سيف! السيرفر شغال وتمام ومستعد لأي تعديل أو أمر تطلبه!")
433
+ except Exception as e:
434
+ send_telegram_message(chat_id, "أمرك يا قائدنا! أنا متصل بالسيرفر وجاهز لأوامرك فوراً!")
435
+
436
+ def process_single_update(update: dict) -> None:
437
+ """Processes a single Telegram update in a non-blocking thread."""
438
+ try:
439
+ msg = update.get("message")
440
+ if not msg or "text" not in msg:
441
+ return
442
+
443
+ chat_id = msg["chat"]["id"]
444
+ text = msg["text"].strip()
445
+
446
+ safe_name = str(msg.get("from", {}).get("first_name", "User"))
447
+ safe_text = text.replace("\n", " ")
448
+ print(f"[TelegramBot] 📩 وصل طلب جديد من القائد {safe_name} ({chat_id}): '{safe_text}'")
449
+
450
+ if chat_id not in _admin_chat_ids:
451
+ _admin_chat_ids.add(chat_id)
452
+ save_admin_chats()
453
+
454
+ # Immediate typing feedback
455
+ send_chat_action(chat_id, "typing")
456
+
457
+ # 1. Fast Command check
458
+ if handle_user_command(chat_id, text):
459
+ return
460
+
461
+ # 2. Natural language conversation with Gemini
462
+ handle_gemini_chat_message(chat_id, text)
463
+ except Exception as ex:
464
+ print(f"[TelegramBot] Error processing update: {ex}")
465
+
466
+ def run_telegram_polling() -> None:
467
+ """Ultra-fast concurrent polling thread for receiving Telegram messages."""
468
+ if not TELEGRAM_TOKEN or TELEGRAM_TOKEN == "YOUR_TELEGRAM_BOT_TOKEN":
469
+ print("[TelegramBot] No bot token configured, polling disabled.")
470
+ return
471
+
472
+ print(f"[TelegramBot] 🚀 Starting Ultra-Fast Telegram Gemini Bot Agent (@cyberfort3d_bot)...")
473
+ last_update_id = 0
474
+
475
+ while True:
476
+ try:
477
+ url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/getUpdates?offset={last_update_id + 1}&timeout=8"
478
+ req = urllib.request.Request(url, headers={"User-Agent": "TitanStrikeBot/2.0"})
479
+ with urllib.request.urlopen(req, timeout=12) as resp:
480
+ data = json.loads(resp.read().decode("utf-8"))
481
+ if not data.get("ok"):
482
+ time.sleep(1)
483
+ continue
484
+
485
+ results = data.get("result", [])
486
+ for update in results:
487
+ last_update_id = update["update_id"]
488
+ # Process each message concurrently in a background thread for < 1s response!
489
+ threading.Thread(target=process_single_update, args=(update,), daemon=True).start()
490
+
491
+ except urllib.error.URLError:
492
+ time.sleep(2)
493
+ except Exception as e:
494
+ time.sleep(1)
495
+
496
+ def start_telegram_bot_thread() -> threading.Thread:
497
+ """Launches the Telegram bot in an independent background daemon thread."""
498
+ t = threading.Thread(target=run_telegram_polling, daemon=True, name="TelegramBotAgent")
499
+ t.start()
500
+ return t
501
+
502
+ if __name__ == "__main__":
503
+ print(f"[*] Running standalone Telegram Bot Agent (@cyberfort3d_bot)...")
504
+ run_telegram_polling()
update_server.py ADDED
@@ -0,0 +1,1859 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CyberFort Game Update & Remote Control Server (OTA Auto-Patcher & Live Game Manager)
4
+ Serves game version metadata, live game configuration (characters, stats, maps),
5
+ and downloadable patch packages (.zip / .pck) for mobile (iOS / Android) and PC clients
6
+ across both Local Wi-Fi and WAN/Internet connections.
7
+ """
8
+
9
+ import http.server
10
+ import socketserver
11
+ import os
12
+ import sys
13
+ import json
14
+ import re
15
+ import time
16
+ import socket
17
+ import urllib.parse
18
+ import urllib.request
19
+ import threading
20
+ import ipaddress
21
+ import base64
22
+ from pathlib import Path
23
+
24
+ if hasattr(sys.stdout, "reconfigure"):
25
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
26
+ if hasattr(sys.stderr, "reconfigure"):
27
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
28
+
29
+ PORT = int(os.environ.get("PORT", 8080))
30
+ GUEST_PORT = int(os.environ.get("GUEST_PORT", 8081 if PORT != 8081 else 8082))
31
+ IS_CLOUD = bool(os.environ.get("RENDER") or os.environ.get("RAILWAY_ENVIRONMENT") or os.environ.get("DYNO") or (os.environ.get("PORT") and os.environ.get("PORT") != "8080"))
32
+ BASE_DIR = Path(__file__).resolve().parent
33
+ PUBLIC_DIR = BASE_DIR / "public" if (BASE_DIR / "public").exists() else (BASE_DIR / "server" / "public" if (BASE_DIR / "server" / "public").exists() else BASE_DIR)
34
+ DOWNLOADS_DIR = PUBLIC_DIR / "downloads"
35
+ VERSION_FILE = PUBLIC_DIR / "version.json"
36
+ GAME_CONFIG_FILE = PUBLIC_DIR / "game_config.json"
37
+
38
+ DOWNLOADS_DIR.mkdir(parents=True, exist_ok=True)
39
+
40
+ DEFAULT_VERSION_DATA = {
41
+ "version": "1.1.3",
42
+ "version_code": 5,
43
+ "release_date": "2026-10-19",
44
+ "title": "تحديث لوحة تحكم الشخصيات ودعم الاتصال عبر الإنترنت",
45
+ "changelog": [
46
+ "إمكانية إزالة أو تفعيل/تعطيل أي شخصية مباشرة من الموقع",
47
+ "تعديل خصائص وسرعة وصحة الشخصيات فورياً من لوحة التحكم",
48
+ "دعم الاتصال بالأجهزة عبر الإنترنت وبيانات الهاتف (WAN & Mobile Data)",
49
+ "تحسين المظهر الجمالي للعبة والقوائم وواجهة المستخدم (Cyber Polish)"
50
+ ],
51
+ "download_url": "/downloads/update.zip",
52
+ "file_size_bytes": 0,
53
+ "sha256": "",
54
+ "mandatory": False
55
+ }
56
+
57
+ DEFAULT_GAME_CONFIG = {
58
+ "heroes": {
59
+ "0": {
60
+ "name": "🍉 Melon Knight (فارس البطيخ)",
61
+ "enabled": True,
62
+ "hp": 130,
63
+ "speed": 7.5,
64
+ "role": "The newly upgraded hero! Fully animated emerald watermelon warrior with ruby cyber-core, fires rapid seeds and builds sturdy walls."
65
+ },
66
+ "1": {
67
+ "name": "🍓 Berry Striker (بطلة الفراولة)",
68
+ "enabled": True,
69
+ "hp": 100,
70
+ "speed": 8.5,
71
+ "role": "Super fast berry ranger with high critical seed shots and tactical speed ramps."
72
+ },
73
+ "2": {
74
+ "name": "🍊 Citrus Tank (البرتقال المدرّع)",
75
+ "enabled": True,
76
+ "hp": 240,
77
+ "speed": 5.6,
78
+ "role": "Heavy armored citrus juggernaut with explosive melon cannons and auto-turrets."
79
+ },
80
+ "3": {
81
+ "name": "👑 Admin (الأدمن الخارق)",
82
+ "enabled": True,
83
+ "hp": 500,
84
+ "speed": 15.0,
85
+ "role": "Legendary secret hero! Unlocked by beating all 3 waves and both Big Bosses. Has 500 HP and golden armor."
86
+ },
87
+ "4": {
88
+ "name": "🕷️ Spider Hero (سبايدر هودي)",
89
+ "enabled": True,
90
+ "hp": 220,
91
+ "speed": 9.2,
92
+ "role": "The newly added 3D Chibi Spider-Hero! Wearing an oversized hoodie, denim cutoffs, and retro high-top sneakers. Highly agile with web attacks!"
93
+ }
94
+ },
95
+ "active_maps": [0, 1, 2, 3, 4, 5, 6],
96
+ "server_announcement": "مرحباً بكم في سيرفر سايبر فورت! جميع الأبطال متاحون للقتال.",
97
+ "server_status": "ONLINE"
98
+ }
99
+
100
+ _cached_wan_ip = None
101
+
102
+ def get_all_lan_ips() -> list[str]:
103
+ """Returns all active local IPv4 addresses sorted by priority (LAN/Wi-Fi first, then Hotspot, vEthernet)."""
104
+ ips = []
105
+ try:
106
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
107
+ s.connect(("8.8.8.8", 80))
108
+ primary = s.getsockname()[0]
109
+ s.close()
110
+ if primary and primary != "127.0.0.1":
111
+ ips.append(primary)
112
+ except Exception:
113
+ pass
114
+
115
+ try:
116
+ import psutil
117
+ for iface, addrs in psutil.net_if_addrs().items():
118
+ for a in addrs:
119
+ if a.family == socket.AF_INET:
120
+ ip = a.address
121
+ if ip and not ip.startswith("127.") and not ip.startswith("169.254."):
122
+ if ip not in ips:
123
+ if not ip.startswith("172."):
124
+ ips.insert(0 if not ips else 1, ip)
125
+ else:
126
+ ips.append(ip)
127
+ except Exception:
128
+ pass
129
+
130
+ if not ips:
131
+ ips.append("127.0.0.1")
132
+ return ips
133
+
134
+ def get_lan_ip() -> str:
135
+ all_lan = get_all_lan_ips()
136
+ return all_lan[0] if all_lan else "127.0.0.1"
137
+
138
+ def get_wan_ip():
139
+ global _cached_wan_ip
140
+ if _cached_wan_ip:
141
+ return _cached_wan_ip
142
+ try:
143
+ req = urllib.request.Request(
144
+ "https://api.ipify.org",
145
+ headers={"User-Agent": "Mozilla/5.0"}
146
+ )
147
+ with urllib.request.urlopen(req, timeout=4) as response:
148
+ _cached_wan_ip = response.read().decode("utf-8").strip()
149
+ return _cached_wan_ip
150
+ except Exception:
151
+ return "105.197.151.212"
152
+
153
+ def is_local_router_client(client_ip: str) -> bool:
154
+ """Returns True if the client IP belongs to the local router subnet (Wi-Fi/LAN/localhost)."""
155
+ try:
156
+ clean_ip = str(client_ip).replace("::ffff:", "").strip()
157
+ if clean_ip in ("127.0.0.1", "::1", "localhost"):
158
+ return True
159
+ ip = ipaddress.ip_address(clean_ip)
160
+ return ip.is_private or ip.is_loopback
161
+ except Exception:
162
+ return False
163
+
164
+ def load_version_info():
165
+ if not VERSION_FILE.exists():
166
+ save_version_info(DEFAULT_VERSION_DATA)
167
+ return DEFAULT_VERSION_DATA
168
+ try:
169
+ with open(VERSION_FILE, "r", encoding="utf-8") as f:
170
+ return json.load(f)
171
+ except Exception as e:
172
+ print(f"[Error] Failed to read version.json: {e}")
173
+ return DEFAULT_VERSION_DATA
174
+
175
+ def save_version_info(data):
176
+ with open(VERSION_FILE, "w", encoding="utf-8") as f:
177
+ json.dump(data, f, indent=2, ensure_ascii=False)
178
+
179
+ def load_game_config():
180
+ if not GAME_CONFIG_FILE.exists():
181
+ save_game_config(DEFAULT_GAME_CONFIG)
182
+ return DEFAULT_GAME_CONFIG
183
+ try:
184
+ with open(GAME_CONFIG_FILE, "r", encoding="utf-8") as f:
185
+ return json.load(f)
186
+ except Exception as e:
187
+ print(f"[Error] Failed to read game_config.json: {e}")
188
+ return DEFAULT_GAME_CONFIG
189
+
190
+ def save_game_config(data):
191
+ with open(GAME_CONFIG_FILE, "w", encoding="utf-8") as f:
192
+ json.dump(data, f, indent=2, ensure_ascii=False)
193
+
194
+ def handle_verify_device_request(data: dict, client_ip: str) -> tuple[int, dict]:
195
+ device_id = str(data.get("device_id", "")).strip()
196
+ device_name = str(data.get("device_name", "Unknown Device")).strip()
197
+ os_name = str(data.get("os", "Unknown OS")).strip()
198
+ username = str(data.get("username", "")).strip()
199
+
200
+ current_config = load_game_config()
201
+ dev_sec = current_config.get("device_security", {
202
+ "whitelist_enabled": True,
203
+ "allowed_devices": [],
204
+ "pending_devices": []
205
+ })
206
+
207
+ if not dev_sec.get("whitelist_enabled", True):
208
+ return 200, {"allowed": True, "message": "Whitelist disabled - access granted"}
209
+
210
+ clean_id = device_id.strip("{} ").lower()
211
+ matched = None
212
+ for dev in dev_sec.get("allowed_devices", []):
213
+ if dev.get("id", "").strip("{} ").lower() == clean_id:
214
+ matched = dev
215
+ break
216
+
217
+ if matched and matched.get("status", "APPROVED") == "APPROVED":
218
+ return 200, {
219
+ "allowed": True,
220
+ "device_id": device_id,
221
+ "device_name": matched.get("name", device_name),
222
+ "message": "Authorized device recognized"
223
+ }
224
+ else:
225
+ pending = dev_sec.get("pending_devices", [])
226
+ exists_pending = any(p.get("id", "").strip("{} ").lower() == clean_id for p in pending)
227
+ if not exists_pending and clean_id:
228
+ pending.append({
229
+ "id": device_id,
230
+ "name": device_name,
231
+ "os": os_name,
232
+ "username": username,
233
+ "ip": client_ip,
234
+ "requested_at": "2026-10-20"
235
+ })
236
+ dev_sec["pending_devices"] = pending
237
+ current_config["device_security"] = dev_sec
238
+ save_game_config(current_config)
239
+
240
+ return 200, {
241
+ "allowed": False,
242
+ "device_id": device_id,
243
+ "reason": "UNAUTHORIZED_DEVICE",
244
+ "message": "⛔ هذا الجهاز غير مصرح له بالدخول. يرجى طلب التصريح من مدير السيرفر."
245
+ }
246
+
247
+ def handle_device_request_submission(data: dict, client_ip: str) -> tuple[int, dict]:
248
+ dev_name = str(data.get("device_name", "📱 جهاز زائر")).strip()
249
+ dev_id = str(data.get("device_id", "")).strip()
250
+ notes = str(data.get("notes", "")).strip()
251
+ clean_id = dev_id.strip("{} ").lower()
252
+
253
+ current_config = load_game_config()
254
+ dev_sec = current_config.get("device_security", {
255
+ "whitelist_enabled": True,
256
+ "allowed_devices": [],
257
+ "pending_devices": []
258
+ })
259
+
260
+ pending = dev_sec.get("pending_devices", [])
261
+ exists = any(p.get("id", "").strip("{} ").lower() == clean_id for p in pending) if clean_id else False
262
+ if not exists:
263
+ if not clean_id:
264
+ import uuid
265
+ dev_id = f"guest-{uuid.uuid4().hex[:8]}"
266
+ pending.append({
267
+ "id": dev_id,
268
+ "name": dev_name,
269
+ "notes": notes,
270
+ "ip": client_ip,
271
+ "requested_at": "2026-10-20"
272
+ })
273
+ dev_sec["pending_devices"] = pending
274
+ current_config["device_security"] = dev_sec
275
+ save_game_config(current_config)
276
+
277
+ return 200, {
278
+ "success": True,
279
+ "device_id": dev_id,
280
+ "message": "Device request registered for admin approval!"
281
+ }
282
+
283
+ # ==============================================================================
284
+ # LIVE GAME CONSOLE & ROOM TRACKING (Leader Real-time Operations)
285
+ # ==============================================================================
286
+ CONSOLE_LOGS = []
287
+ MAX_CONSOLE_LOGS = 250
288
+ ROOMS = {} # room_code: { "code": code, "host": host, "mode": mode, "players": count, "created_at": time, "status": "ACTIVE" }
289
+ TOTAL_ROOMS_CREATED = 0
290
+ TERMINATED_ROOMS = set()
291
+ ACTIVE_SESSIONS = {} # session_id: { "player": ..., "mode": ..., "hero": ..., "status": ..., "ip": ... }
292
+ GLOBAL_EMERGENCY_ALARM = {"active": False, "message": "", "id": 0}
293
+ GLOBAL_BROADCAST = {"message": "", "id": 0, "room_code": ""}
294
+ GLOBAL_AI_DUEL_TRIGGER = {"active": False, "message": "", "room_code": "AI1V1", "ai_name": "CyberAI", "id": 0}
295
+
296
+ # ==============================================================================
297
+ # GEMINI 3.5 FLASH AI INTEGRATION (CyberAI Engine)
298
+ # ==============================================================================
299
+ GEMINI_B64_KEY = "QVEuQWI4Uk42SW9IRnU1M000UkJBTlR6ZHcxaG1xQkJnckZTdXlzNzNwT3pfTUJ5R01FR0E="
300
+
301
+ def _resolve_gemini_key():
302
+ env_k = os.environ.get("GEMINI_API_KEY")
303
+ if env_k:
304
+ return env_k
305
+ cfg_file = BASE_DIR / "ai_config.json"
306
+ if cfg_file.exists():
307
+ try:
308
+ with open(cfg_file, "r", encoding="utf-8") as f:
309
+ d = json.load(f)
310
+ if d.get("api_key"):
311
+ return d["api_key"]
312
+ except Exception:
313
+ pass
314
+ try:
315
+ return base64.b64decode(GEMINI_B64_KEY).decode("ascii")
316
+ except Exception:
317
+ return ""
318
+
319
+ GEMINI_API_KEY = _resolve_gemini_key()
320
+ GEMINI_MODEL = "gemini-3.5-flash-lite"
321
+ GEMINI_FALLBACK_MODELS = ["gemini-3.5-flash-lite", "gemini-flash-lite-latest", "gemini-3.5-flash"]
322
+
323
+
324
+ def execute_server_action(action_name: str, params: dict, context_type: str = "general") -> tuple[bool, str, dict]:
325
+ global ROOMS, TOTAL_ROOMS_CREATED, GLOBAL_BROADCAST, GLOBAL_EMERGENCY_ALARM
326
+ is_admin = (context_type == "admin_dashboard")
327
+ action_data = {"name": action_name, "params": params}
328
+
329
+ global GLOBAL_AI_DUEL_TRIGGER
330
+ act_l = str(action_name).lower()
331
+ if any(k in act_l for k in ["room", "1v1", "duel", "game", "ai_room"]):
332
+ hero_name = params.get("hero", "Spider Hero")
333
+ code = "AI1V1"
334
+ now_ts = time.time()
335
+ import datetime
336
+ now_str = datetime.datetime.now().strftime("%H:%M:%S")
337
+ all_lan = get_all_lan_ips()
338
+ clean_lan = [ip for ip in all_lan if ip != "127.0.0.1"]
339
+ host_ip = clean_lan[0] if clean_lan else "127.0.0.1"
340
+
341
+ ROOMS[code] = {
342
+ "code": code,
343
+ "host": "CyberAI 🤖 [BOT 1v1]",
344
+ "mode": "⚔️ 1v1 PvP Duel vs CyberAI",
345
+ "map_id": 0,
346
+ "players": 1,
347
+ "players_list": ["CyberAI 🤖"],
348
+ "created_at": now_str,
349
+ "host_ip": host_ip,
350
+ "candidate_ips": all_lan,
351
+ "port": 7777,
352
+ "last_seen": now_ts,
353
+ "status": "WAITING_FOR_OPPONENT",
354
+ "is_ai_room": True,
355
+ "ai_hero": hero_name
356
+ }
357
+ TOTAL_ROOMS_CREATED += 1
358
+
359
+ GLOBAL_AI_DUEL_TRIGGER["active"] = True
360
+ GLOBAL_AI_DUEL_TRIGGER["id"] = int(time.time() * 1000)
361
+ GLOBAL_AI_DUEL_TRIGGER["room_code"] = code
362
+ GLOBAL_AI_DUEL_TRIGGER["message"] = f"⚔️ أنشأ CyberAI غرفة 1v1 بكود #{code} ويدعوك للنزال فوراً!"
363
+
364
+ GLOBAL_BROADCAST["message"] = f"⚔️ أنشأ CyberAI غرفة 1v1 بكود #{code} ويدعوك لدخول الساحة فوراً!"
365
+ GLOBAL_BROADCAST["id"] = int(time.time() * 1000)
366
+ GLOBAL_BROADCAST["room_code"] = code
367
+
368
+ msg = f"تم إنشاء وتجهيز غرفة المبارزة 1v1 بنجاح على السيرفر! كود الغرفة: #{code}"
369
+ add_console_log("CYBER_AI", f"⚔️ دخل CyberAI السيرفر وأنشأ غرفة 1v1 بكود #{code} بانتظار القائد في الساحة فوراً!", "SUCCESS")
370
+ return True, msg, action_data
371
+
372
+ elif any(k in act_l for k in ["broadcast", "announce", "msg", "message"]):
373
+ if not is_admin:
374
+ return False, "عذراً! إرسال إعلانات عامة باللعبة متاح للقائد فقط من لوحة التحكم.", action_data
375
+ b_msg = str(params.get("message", "")).strip()
376
+ if not b_msg:
377
+ return False, "نص الإعلان فارغ.", action_data
378
+ GLOBAL_BROADCAST["message"] = b_msg
379
+ GLOBAL_BROADCAST["id"] = int(time.time())
380
+ GLOBAL_BROADCAST["room_code"] = ""
381
+ add_console_log("ADMIN", f"📢 أرسل CyberAI إعلان القائد باللعبة: '{b_msg}'", "SUCCESS")
382
+ return True, f"تم إرسال إعلان القائد لجميع اللاعبين باللعبة: '{b_msg}'", action_data
383
+
384
+ elif any(k in act_l for k in ["alarm", "siren"]):
385
+ if not is_admin:
386
+ return False, "عذراً! التحكم بصفارات الإنذار متاح للقائد فقط.", action_data
387
+ active = bool(params.get("active", True))
388
+ msg = str(params.get("message", "🚨 إنذار عام صادر من القائد!")).strip()
389
+ GLOBAL_EMERGENCY_ALARM["active"] = active
390
+ GLOBAL_EMERGENCY_ALARM["message"] = msg if active else ""
391
+ GLOBAL_EMERGENCY_ALARM["id"] = int(time.time())
392
+ status_txt = "تشغيل" if active else "إيقاف"
393
+ add_console_log("ADMIN", f"🚨 قام CyberAI بـ {status_txt} صفارات الإنذار بأمر من القائد!", "WARNING" if active else "INFO")
394
+ return True, f"تم {status_txt} صفارة الإنذار العامة باللعبة بنجاح.", action_data
395
+
396
+ elif any(k in act_l for k in ["server", "toggle", "maintenance", "open_server", "close_server"]):
397
+ if not is_admin:
398
+ return False, "عذراً! فتح أو إغلاق السيرفر متاح للقائد فقط.", action_data
399
+ target_status = str(params.get("status", "OPEN")).upper()
400
+ if target_status not in ["OPEN", "CLOSED"]:
401
+ target_status = "OPEN"
402
+ cfg = load_game_config()
403
+ cfg["server_status"] = target_status
404
+ save_game_config(cfg)
405
+ status_ar = "مفتوح ومتاح للجميع 🟢" if target_status == "OPEN" else "مغلق للصيانة 🔴"
406
+ add_console_log("ADMIN", f"⚙️ قام CyberAI بتغيير حالة السيرفر بأمر من القائد إلى: {target_status}", "WARNING" if target_status == "CLOSED" else "SUCCESS")
407
+ return True, f"تم تعديل حالة السيرفر إلى: {status_ar}", action_data
408
+
409
+ elif any(k in act_l for k in ["hero", "stat", "balance", "hp", "speed"]):
410
+ if not is_admin:
411
+ return False, "عذراً! تعديل طاقات الأبطال متاح للقائد فقط.", action_data
412
+ hero_id = str(params.get("hero_id", 0))
413
+ hp = params.get("hp")
414
+ speed = params.get("speed")
415
+ cfg = load_game_config()
416
+ heroes = cfg.get("heroes", {})
417
+ if hero_id in heroes:
418
+ if hp is not None:
419
+ heroes[hero_id]["hp"] = int(hp)
420
+ if speed is not None:
421
+ heroes[hero_id]["speed"] = float(speed)
422
+ save_game_config(cfg)
423
+ h_name = heroes[hero_id].get("name", f"Hero #{hero_id}")
424
+ add_console_log("ADMIN", f"⚖️ قام CyberAI بتعديل طاقة البطل ({h_name}) بأمر من القائد!", "INFO")
425
+ return True, f"تم تعديل طاقة {h_name} بنجاح.", action_data
426
+ return False, "لم يتم العثور على البطل المطلوب.", action_data
427
+
428
+ elif any(k in act_l for k in ["terminate", "kick", "delete"]):
429
+ if not is_admin:
430
+ return False, "عذراً! إغلاق الغرف متاح للقائد فقط.", action_data
431
+ code = str(params.get("code", "")).strip().upper()
432
+ if code:
433
+ handle_rooms_terminate({"room_code": code})
434
+ return True, f"تم طرد اللاعبين وإغلاق الغرفة #{code} بنجاح.", action_data
435
+ return False, "كود الغرفة غير صحيح.", action_data
436
+
437
+ return False, "أمر غير معروف.", action_data
438
+
439
+
440
+ def detect_user_intent_action(prompt: str, context_type: str = "general") -> dict:
441
+ p = prompt.strip().lower()
442
+ is_admin = (context_type == "admin_dashboard")
443
+
444
+ # 1. Create Room / Enter Game / 1v1 Duel against CyberAI
445
+ create_room_keywords = [
446
+ "خش اللعبة", "خش العبه", "ادخل اللعبة", "ادخل العبه", "اعمل غرفة", "اعمل الغرفة",
447
+ "اعمل روم", "اعمل لي روم", "اعمل غرفه", "اعمل الغرفه", "اعمل الغرف", "اعمل لي الغرف",
448
+ "سوي روم", "سوي غرفة", "سوي غرفه", "سوي الغرف", "افتح روم", "افتح غرفة", "افتح غرفه", "افتح الغرف",
449
+ "روم 1v1", "1v1", "العب معاك", "العب معايا", "نلعب سوا", "نلعب مع بعض",
450
+ "نلعب ضد بعض", "قولي الكود", "كود الغرفة", "كود الغرفه", "مبارزة", "تحدي", "روم", "غرفة", "غرفه", "الغرف", "غرف"
451
+ ]
452
+ if any(k in p for k in create_room_keywords):
453
+ return {"name": "create_ai_room", "params": {"hero": "Spider Hero"}}
454
+
455
+ # Admin only actions
456
+ if is_admin:
457
+ if "إنذار" in p or "انذار" in p or "صفارة" in p:
458
+ active = not any(k in p for k in ["اطفي", "وقف", "إيقاف", "الغاء", "إلغاء"])
459
+ return {"name": "alarm", "params": {"active": active}}
460
+
461
+ if ("اقفل" in p or "قفل" in p or "اغلق" in p or "صيانة" in p) and ("سيرفر" in p or "السيرفر" in p or "سرفر" in p or "السرفر" in p):
462
+ return {"name": "server_toggle", "params": {"status": "CLOSED"}}
463
+
464
+ if ("افتح" in p or "تشغيل" in p) and ("سيرفر" in p or "السيرفر" in p or "سرفر" in p or "السرفر" in p):
465
+ return {"name": "server_toggle", "params": {"status": "OPEN"}}
466
+
467
+ if any(k in p for k in ["رسالة للاعبين", "اعلان للاعبين", "إعلان للاعبين"]) or (p.startswith("ابعت") and "لاعبين" in p):
468
+ msg = p
469
+ for prefix in ["ابعت رسالة للاعبين:", "ابعت رسالة للاعبين", "اكتب اعلان للاعبين:", "اكتب إعلان للاعبين:", "أعلن:"]:
470
+ if prefix in msg:
471
+ msg = msg.split(prefix, 1)[1].strip()
472
+ break
473
+ return {"name": "broadcast", "params": {"message": msg}}
474
+
475
+ return None
476
+
477
+ def call_gemini_ai(user_prompt: str, context_type: str = "general") -> tuple[int, dict]:
478
+ """Generates intelligent game responses using Google Gemini 3.5 Flash with full executive action control."""
479
+ user_prompt = str(user_prompt).strip()
480
+ if not user_prompt:
481
+ return 400, {"success": False, "error": "يرجى كتابة سؤال أو استفسار للذكاء الاصطناعي."}
482
+
483
+ is_admin = (context_type == "admin_dashboard")
484
+
485
+ if is_admin:
486
+ sys_context = (
487
+ "أنت 'CyberAI' - المساعد الذكي لـ 'القائد' في لعبة Titan Strike 3D.\n"
488
+ "مهمتك وصلاحياتك محددة في أمرين اثنين فقط لا غير بأمر القائد:\n"
489
+ "1. الرد بدقة وذكاء وسرعة على أي أسئلة يطرحها القائد (سواء عن اللعبة، الأبطال، التكتيكات، حالة السيرفر، أو أي سؤال عام).\n"
490
+ "2. تنفيذ الأوامر داخل اللعبة حصراً: (إنشاء غرفة مبارزة 1v1 ودخول الساحة، إرسال إعلان للاعبين باللعبة، تشغيل أو إيقاف صفارات الإنذار، فتح أو إغلاق السيرفر).\n"
491
+ "⚠️ تنبيه صارم: لا تقم بأي تعديل على ملفات الكود أو حذف ملفات النظام أو أي إجراء خارج اللعبة. عملك محصور فقط في الإجابة على الأسئلة ثم تنفيذ فعاليات داخل اللعبة فقط!\n"
492
+ "إذا طلب منك القائد دخول اللعبة أو عمل غرفة 1v1، رحب بأمره فوراً وأخبره بكود الغرفة: #AI1V1 لتحديه في الساحة!\n"
493
+ "إذا طلب القائد أي أمر تنفيذي داخل اللعبة، قم بتضمين تاج الأمر: <!--ACTION:{\"name\": \"<action_name>\", \"params\": {..}}-->\n"
494
+ "أجب دائماً بلغة عربية حماسية ومباشرة وذكية في سطرين إلى 3 أسطر كحد أقصى."
495
+ )
496
+ else:
497
+ sys_context = (
498
+ "أنت 'CyberAI' - المساعد التكتيكي الذكي للعبة سايبر فورت (Titan Strike 3D). "
499
+ "الشخص الذي يتحدث معك الآن هو 'لاعب عادي / مقاتل'. خاطبه بلقب 'يا بطل' أو 'يا مقاتل'. "
500
+ "أنت تساعده في أسرار اللعب، نصائح الأبطال، استراتيجيات الفوز، ويمكنك التحدي معه في مبارزة 1v1. "
501
+ "إذا طلب منك اللعب معه أو عمل غرفة 1v1 ('العب معايا'، 'خش اللعبة واعمل غرفة وقولي الكود')، رحب بالتحدي بحماس وأعطه كود الغرفة: #AI1V1 وقل له أنك بانتظاره في ساحة القتال! "
502
+ "إذا طلب اللاعب أوامر إدارية (مثل إغلاق السيرفر أو تغيير طاقات الأبطال أو طرد لاعبين)، ارفض بلباقة وأخبره أن هذه الصلاحيات الإدارية حصرية فقط لـ 'القائد' من لوحة التحكم في الموقع. "
503
+ "أجب دائماً بلغة عربية حماسية ومباشرة في سطرين إلى 3 أسطر."
504
+ )
505
+
506
+ payload = {
507
+ "contents": [
508
+ {
509
+ "parts": [
510
+ {"text": f"[تعليمات النظام]: {sys_context}\n\n[طلب {'القائد' if is_admin else 'اللاعب'}]: {user_prompt}"}
511
+ ]
512
+ }
513
+ ],
514
+ "generationConfig": {
515
+ "temperature": 0.7,
516
+ "maxOutputTokens": 300
517
+ }
518
+ }
519
+ encoded_body = json.dumps(payload).encode("utf-8")
520
+
521
+ reply_text = ""
522
+ used_model = "gemini-3.5-flash-lite"
523
+
524
+ for m in GEMINI_FALLBACK_MODELS:
525
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent?key={GEMINI_API_KEY}"
526
+ try:
527
+ req = urllib.request.Request(
528
+ url,
529
+ data=encoded_body,
530
+ headers={"Content-Type": "application/json"}
531
+ )
532
+ with urllib.request.urlopen(req, timeout=8) as resp:
533
+ data = json.loads(resp.read().decode("utf-8"))
534
+ candidates = data.get("candidates", [])
535
+ if candidates and "content" in candidates[0]:
536
+ parts = candidates[0]["content"].get("parts", [])
537
+ reply_text = "".join(p.get("text", "") for p in parts).strip()
538
+ used_model = m
539
+ break
540
+ except Exception:
541
+ continue
542
+
543
+ if not reply_text:
544
+ # Fallback offline smart response
545
+ if is_admin:
546
+ reply_text = "أمرك يا قائدنا! تم استلام طلبك وجاري تطبيقه في السيرفر فوراً."
547
+ else:
548
+ reply_text = "أهلاً بك يا بطل! أنا جاهز لمساعدتك في أي استفسار أو التحدي في الساحة!"
549
+
550
+ # Parse and execute actions
551
+ action_info = None
552
+ action_msg = ""
553
+
554
+ # 1. Check for action tag in Gemini response
555
+ m_action = re.search(r'<!--ACTION:(.*?)-->', reply_text, re.DOTALL)
556
+ if m_action:
557
+ try:
558
+ raw_act = json.loads(m_action.group(1).strip())
559
+ act_name = raw_act.get("name")
560
+ act_params = raw_act.get("params", {})
561
+ reply_text = reply_text.replace(m_action.group(0), "").strip()
562
+ ok, action_msg, action_info = execute_server_action(act_name, act_params, context_type)
563
+ except Exception as ex:
564
+ print("Action parse error:", ex)
565
+
566
+ # 2. Safety Net: If no action parsed, use intent detection
567
+ if not action_info:
568
+ detected = detect_user_intent_action(user_prompt, context_type)
569
+ if detected:
570
+ ok, action_msg, action_info = execute_server_action(detected["name"], detected.get("params", {}), context_type)
571
+
572
+ if action_info and any(k in str(action_info.get("name", "")).lower() for k in ["room", "1v1", "duel"]):
573
+ reply_text = (
574
+ "أمرك يا قائدنا وسمعاً وطاعة! ⚔️\n"
575
+ "دخلت اللعبة وجهزتلك غرفة المبارزة 1v1 فوراً كخصم ومنافس لك في الساحة!\n"
576
+ "🔑 كود الغرفة: #AI1V1\n"
577
+ "تم إرسال إشعار التحدي المباشر لشاشة لعبتك الآن، أو اضغط على الزر بالأسفل للبدء فوراً!"
578
+ )
579
+
580
+ add_console_log("CYBER_AI", f"🤖 ({'👑 القائد' if is_admin else '👤 اللاعب'}): '{user_prompt[:30]}...' -> {reply_text[:40]}...", "SUCCESS" if action_info else "INFO")
581
+
582
+ return 200, {
583
+ "success": True,
584
+ "reply": reply_text,
585
+ "model": used_model,
586
+ "role": "commander" if is_admin else "player",
587
+ "action": action_info,
588
+ "action_result": action_msg
589
+ }
590
+
591
+
592
+ def add_console_log(source: str, message: str, level: str = "INFO"):
593
+ global CONSOLE_LOGS
594
+ import datetime
595
+ now_str = datetime.datetime.now().strftime("%H:%M:%S")
596
+ entry = {
597
+ "time": now_str,
598
+ "source": source,
599
+ "message": message,
600
+ "level": level
601
+ }
602
+ CONSOLE_LOGS.append(entry)
603
+ if len(CONSOLE_LOGS) > MAX_CONSOLE_LOGS:
604
+ CONSOLE_LOGS.pop(0)
605
+
606
+ # Instant Telegram Alert on any server or game error
607
+ if level == "ERROR":
608
+ try:
609
+ from telegram_bot import notify_server_error
610
+ notify_server_error(source, message)
611
+ except Exception:
612
+ pass
613
+
614
+ def handle_session_update(data: dict, client_ip: str) -> tuple[int, dict]:
615
+ global ACTIVE_SESSIONS
616
+ session_id = str(data.get("session_id", "")).strip()
617
+ if not session_id:
618
+ session_id = f"sess_{str(client_ip).replace(':', '_')}"
619
+
620
+ action = str(data.get("action", "heartbeat")).strip()
621
+ if action == "leave":
622
+ removed = ACTIVE_SESSIONS.pop(session_id, None)
623
+ if removed:
624
+ add_console_log("SESSION", f"👋 خرج اللاعب '{removed.get('player', '')}' من الجلسة", "INFO")
625
+ return 200, {"success": True, "active_count": len(ACTIVE_SESSIONS)}
626
+
627
+ import datetime
628
+ import time
629
+ now_str = datetime.datetime.now().strftime("%H:%M:%S")
630
+ clean_ip = str(client_ip).replace("::ffff:", "").strip()
631
+
632
+ is_new = session_id not in ACTIVE_SESSIONS
633
+ ACTIVE_SESSIONS[session_id] = {
634
+ "id": session_id,
635
+ "player": str(data.get("player_name", "لاعب")).strip(),
636
+ "mode": str(data.get("mode", "⚔️ 1v1 PvP Duel")).strip(),
637
+ "hero": str(data.get("hero", "Melon Boy")).strip(),
638
+ "status": str(data.get("status", "PLAYING")).strip(),
639
+ "ip": clean_ip,
640
+ "time": now_str,
641
+ "last_seen": time.time()
642
+ }
643
+ if is_new:
644
+ p_name = ACTIVE_SESSIONS[session_id]["player"]
645
+ m_name = ACTIVE_SESSIONS[session_id]["mode"]
646
+ add_console_log("SESSION", f"🎮 دخل اللاعب '{p_name}' طور ({m_name}) الآن!", "SUCCESS")
647
+
648
+ return 200, {"success": True, "active_count": len(ACTIVE_SESSIONS), "sessions": list(ACTIVE_SESSIONS.values())}
649
+
650
+ def handle_rooms_update(data: dict) -> tuple[int, dict]:
651
+ global ROOMS, TOTAL_ROOMS_CREATED
652
+ code = str(data.get("room_code", "")).strip().upper()
653
+ if not code:
654
+ return 400, {"success": False, "error": "Missing room code"}
655
+
656
+ import datetime
657
+ import time
658
+ now_ts = time.time()
659
+
660
+ if code in ROOMS:
661
+ ROOMS[code]["last_seen"] = now_ts
662
+ if "mode" in data and data["mode"]:
663
+ old_mode = ROOMS[code].get("mode", "")
664
+ new_mode = str(data["mode"]).strip()
665
+ ROOMS[code]["mode"] = new_mode
666
+ if old_mode != new_mode:
667
+ add_console_log("ROOM", f"🔄 تم تغيير طور الغرفة #{code} إلى ({new_mode})", "INFO")
668
+ if "players_list" in data:
669
+ ROOMS[code]["players_list"] = data["players_list"]
670
+ if "players" in data and data["players"] is not None:
671
+ p_cnt = int(data["players"])
672
+ if "players_list" in ROOMS[code] and len(ROOMS[code]["players_list"]) > p_cnt:
673
+ p_cnt = len(ROOMS[code]["players_list"])
674
+ ROOMS[code]["players"] = max(1, p_cnt)
675
+ if "candidate_ips" in data and data["candidate_ips"]:
676
+ ROOMS[code]["candidate_ips"] = data["candidate_ips"]
677
+ if "status" in data and data["status"]:
678
+ ROOMS[code]["status"] = str(data["status"]).strip()
679
+ return 200, {"success": True, "room": ROOMS[code]}
680
+ else:
681
+ # Re-register room if host is still running
682
+ p_list = data.get("players_list", [])
683
+ ROOMS[code] = {
684
+ "code": code,
685
+ "host": str(data.get("host_name", "لاعب")).strip(),
686
+ "mode": str(data.get("mode", "Multiplayer")).strip(),
687
+ "players": max(1, len(p_list), int(data.get("players", 1))),
688
+ "players_list": p_list,
689
+ "created_at": datetime.datetime.now().strftime("%H:%M:%S"),
690
+ "host_ip": get_lan_ip(),
691
+ "port": 7777,
692
+ "status": str(data.get("status", "ACTIVE")).strip(),
693
+ "broadcast_message": "",
694
+ "message_id": 0,
695
+ "last_seen": now_ts
696
+ }
697
+ TOTAL_ROOMS_CREATED += 1
698
+ return 200, {"success": True, "room": ROOMS[code]}
699
+
700
+ def handle_rooms_create(data: dict, client_ip: str = "") -> tuple[int, dict]:
701
+ global TOTAL_ROOMS_CREATED, ROOMS, TERMINATED_ROOMS
702
+ code = str(data.get("room_code", "")).strip().upper()
703
+ host = str(data.get("host_name", "لاعب")).strip()
704
+ mode = str(data.get("mode", "Co-op")).strip()
705
+ players = int(data.get("players", 1))
706
+
707
+ if not code:
708
+ return 400, {"success": False, "error": "Missing room code"}
709
+
710
+ import datetime
711
+ import time
712
+ now_str = datetime.datetime.now().strftime("%H:%M:%S")
713
+
714
+ clean_ip = str(client_ip).replace("::ffff:", "").strip()
715
+ if not clean_ip or clean_ip in ("127.0.0.1", "localhost", "::1"):
716
+ clean_ip = get_lan_ip()
717
+
718
+ p_list = data.get("players_list", [host])
719
+ if not p_list:
720
+ p_list = [host]
721
+
722
+ candidate_ips = data.get("candidate_ips", [])
723
+ if not candidate_ips:
724
+ candidate_ips = [clean_ip]
725
+ elif clean_ip not in candidate_ips and clean_ip not in ("127.0.0.1", "localhost"):
726
+ candidate_ips.insert(0, clean_ip)
727
+
728
+ # Include all server local interface IPs
729
+ for sip in get_all_lan_ips():
730
+ if sip not in candidate_ips and sip != "127.0.0.1":
731
+ candidate_ips.append(sip)
732
+
733
+ TERMINATED_ROOMS.discard(code)
734
+ ROOMS[code] = {
735
+ "code": code,
736
+ "host": host,
737
+ "mode": mode,
738
+ "players": max(len(p_list), players),
739
+ "players_list": p_list,
740
+ "created_at": now_str,
741
+ "host_ip": clean_ip,
742
+ "candidate_ips": candidate_ips,
743
+ "port": int(data.get("port", 7777)),
744
+ "status": "ACTIVE",
745
+ "broadcast_message": "",
746
+ "message_id": 0,
747
+ "last_seen": time.time()
748
+ }
749
+ TOTAL_ROOMS_CREATED += 1
750
+ add_console_log("ROOM", f"🎮 تم إنشاء الغرفة #{code} بواسطة المضيف '{host}' (IP: {clean_ip} | الطور: {mode})", "SUCCESS")
751
+ return 200, {
752
+ "success": True,
753
+ "room": ROOMS[code],
754
+ "total_rooms_created": TOTAL_ROOMS_CREATED,
755
+ "active_rooms_count": len(ROOMS)
756
+ }
757
+
758
+ def handle_rooms_close(data: dict) -> tuple[int, dict]:
759
+ global ROOMS
760
+ code = str(data.get("room_code", "")).strip().upper()
761
+ if code in ROOMS:
762
+ room = ROOMS.pop(code)
763
+ add_console_log("ROOM", f"🚪 تم إغلاق الغرفة #{code} بواسطة المضيف '{room.get('host', '')}'", "INFO")
764
+ return 200, {"success": True, "active_rooms_count": len(ROOMS)}
765
+
766
+ def handle_rooms_terminate(data: dict) -> tuple[int, dict]:
767
+ global ROOMS, TERMINATED_ROOMS, ACTIVE_SESSIONS
768
+ code = str(data.get("room_code", "")).strip().upper()
769
+ if not code:
770
+ return 400, {"success": False, "error": "Missing room code"}
771
+
772
+ TERMINATED_ROOMS.add(code)
773
+ room = ROOMS.pop(code, None)
774
+
775
+ # Also pop any matching session or players belonging to this room
776
+ removed_sessions = []
777
+ for s_id in list(ACTIVE_SESSIONS.keys()):
778
+ s = ACTIVE_SESSIONS[s_id]
779
+ if code in s.get("mode", "") or code == s.get("id", "") or code in s.get("player", ""):
780
+ removed_sessions.append(ACTIVE_SESSIONS.pop(s_id))
781
+
782
+ host_name = room.get("host", "") if room else ""
783
+ if not host_name and removed_sessions:
784
+ host_name = removed_sessions[0].get("player", "لاعب")
785
+ if not host_name:
786
+ host_name = "اللاعبين"
787
+
788
+ add_console_log("ADMIN", f"🛑 قام القائد بقفل وإغلاق الغرفة #{code} وطرد جميع لاعبيها فوراً (المضيف: {host_name})!", "ERROR")
789
+ return 200, {
790
+ "success": True,
791
+ "message": f"تم إغلاق الغرفة #{code} وطرد اللاعبين بنجاح وفصلهم!",
792
+ "active_rooms_count": len(ROOMS)
793
+ }
794
+
795
+ def handle_rooms_message(data: dict) -> tuple[int, dict]:
796
+ global ROOMS, GLOBAL_BROADCAST
797
+ code = str(data.get("room_code", "")).strip().upper()
798
+ msg = str(data.get("message", "")).strip()
799
+ if not msg:
800
+ return 400, {"success": False, "error": "رسالة فارغة"}
801
+
802
+ import time
803
+ msg_id = int(time.time() * 1000)
804
+
805
+ GLOBAL_BROADCAST = {
806
+ "message": msg,
807
+ "id": msg_id,
808
+ "room_code": code
809
+ }
810
+
811
+ for r_code in ROOMS:
812
+ ROOMS[r_code]["broadcast_message"] = msg
813
+ ROOMS[r_code]["message_id"] = msg_id
814
+
815
+ if not code or code == "ALL":
816
+ add_console_log("ADMIN", f"📢 أرسل القائد كلمة في نص الشاشة لجميع اللاعبين: '{msg}'", "SUCCESS")
817
+ return 200, {"success": True, "message": f"تم إرسال '{msg}' في نص شاشات اللاعبين فوراً!"}
818
+
819
+ if code in ROOMS:
820
+ add_console_log("ADMIN", f"💬 أرسل القائد كلمة للاعبي الغرفة #{code}: '{msg}'", "INFO")
821
+ return 200, {"success": True, "message": f"تم إرسال '{msg}' في نص الشاشة بنجاح!"}
822
+ else:
823
+ add_console_log("ADMIN", f"📢 أرسل القائد كلمة للاعبين: '{msg}'", "INFO")
824
+ return 200, {"success": True, "message": f"تم إرسال '{msg}' في نص الشاشة بنجاح!"}
825
+
826
+ def handle_rooms_check(code: str) -> tuple[int, dict]:
827
+ global GLOBAL_EMERGENCY_ALARM, GLOBAL_BROADCAST
828
+ code = code.strip().upper()
829
+ alarm_info = dict(GLOBAL_EMERGENCY_ALARM)
830
+ b_msg = GLOBAL_BROADCAST.get("message", "")
831
+ b_id = GLOBAL_BROADCAST.get("id", 0)
832
+ if code in TERMINATED_ROOMS:
833
+ return 200, {
834
+ "code": code,
835
+ "active": False,
836
+ "terminated": True,
837
+ "emergency_alarm": alarm_info,
838
+ "message": b_msg,
839
+ "message_id": b_id,
840
+ "broadcast_message": b_msg,
841
+ "broadcast_id": b_id
842
+ }
843
+ if code in ROOMS:
844
+ import time
845
+ ROOMS[code]["last_seen"] = time.time()
846
+ room = ROOMS[code]
847
+ r_msg = room.get("broadcast_message", "") or b_msg
848
+ r_id = room.get("message_id", 0) or b_id
849
+ return 200, {
850
+ "code": code,
851
+ "active": True,
852
+ "terminated": False,
853
+ "host_ip": room.get("host_ip", ""),
854
+ "candidate_ips": room.get("candidate_ips", [room.get("host_ip", "")]),
855
+ "port": room.get("port", 7777),
856
+ "message": r_msg,
857
+ "message_id": r_id,
858
+ "broadcast_message": r_msg,
859
+ "broadcast_id": r_id,
860
+ "emergency_alarm": alarm_info
861
+ }
862
+ return 200, {
863
+ "code": code,
864
+ "active": False,
865
+ "terminated": False,
866
+ "emergency_alarm": alarm_info,
867
+ "message": b_msg,
868
+ "message_id": b_id,
869
+ "broadcast_message": b_msg,
870
+ "broadcast_id": b_id
871
+ }
872
+
873
+ def send_json_response(handler, data, status_code: int = 200):
874
+ body = json.dumps(data, ensure_ascii=False).encode("utf-8")
875
+ handler.send_response(status_code)
876
+ handler.send_header("Content-Type", "application/json; charset=utf-8")
877
+ handler.send_header("Content-Length", str(len(body)))
878
+ handler.send_header("Access-Control-Allow-Origin", "*")
879
+ handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
880
+ handler.send_header("Access-Control-Allow-Headers", "Content-Type")
881
+ handler.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
882
+ handler.end_headers()
883
+ handler.wfile.write(body)
884
+
885
+ class UpdateRequestHandler(http.server.SimpleHTTPRequestHandler):
886
+ protocol_version = "HTTP/1.1"
887
+
888
+ def __init__(self, *args, **kwargs):
889
+ super().__init__(*args, directory=str(PUBLIC_DIR), **kwargs)
890
+
891
+ def check_router_access(self) -> bool:
892
+ """Blocks access to the website & admin controls if client is not on the local router Wi-Fi/LAN."""
893
+ if IS_CLOUD or os.environ.get("DISABLE_ROUTER_CHECK", "").lower() in ("1", "true", "yes"):
894
+ return True
895
+ client_ip = self.client_address[0]
896
+ if not is_local_router_client(client_ip):
897
+ self.send_response(403)
898
+ self.send_header("Content-Type", "text/html; charset=utf-8")
899
+ self.send_header("Access-Control-Allow-Origin", "*")
900
+ self.end_headers()
901
+ blocked_html = f"""<!DOCTYPE html>
902
+ <html dir="rtl" lang="ar">
903
+ <head>
904
+ <meta charset="utf-8">
905
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
906
+ <title>403 - الوصول محظور (خاص براوتر الشبكة المحلية فقط)</title>
907
+ <style>
908
+ body {{ background: #070a12; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 20px; box-sizing: border-box; text-align: center; }}
909
+ .card {{ background: rgba(239, 68, 68, 0.08); border: 2px solid #ef4444; border-radius: 20px; padding: 40px 30px; max-width: 540px; box-shadow: 0 0 50px rgba(239, 68, 68, 0.25); }}
910
+ .badge {{ display: inline-block; background: #ef4444; color: #fff; padding: 8px 18px; border-radius: 30px; font-weight: bold; font-size: 14px; margin-bottom: 20px; }}
911
+ h1 {{ color: #ff5c5c; margin: 0 0 15px; font-size: 26px; }}
912
+ p {{ color: #cbd5e1; font-size: 16px; line-height: 1.7; margin: 0 0 20px; }}
913
+ .info-box {{ background: rgba(0, 0, 0, 0.4); border-radius: 12px; padding: 14px; font-size: 14px; color: #94a3b8; font-family: monospace; }}
914
+ </style>
915
+ </head>
916
+ <body>
917
+ <div class="card">
918
+ <div class="badge">🛡️ حماية شبكة الراوتر مفعلة</div>
919
+ <h1>⛔ محظور: الموقع متاح فقط من الراوتر</h1>
920
+ <p>عذراً! هذا الموقع ولوحة التحكم مخصصة <b>حصرياً للأجهزة المتصلة براوتر الواي فاي المنزلي (Local Wi-Fi / Router)</b>.</p>
921
+ <p>لا يُسمح لأي جهاز خارج نطاق الراوتر بالدخول إلى لوحة التحكم أو الموقع.</p>
922
+ <div class="info-box">عنوان IP جهازك: <b>{client_ip}</b> (خارج شبكة الراوتر)</div>
923
+ </div>
924
+ </body>
925
+ </html>"""
926
+ self.wfile.write(blocked_html.encode("utf-8"))
927
+ return False
928
+ return True
929
+
930
+ def do_GET(self):
931
+ parsed = urllib.parse.urlparse(self.path)
932
+ path = parsed.path
933
+
934
+ if path == "/api/version":
935
+ send_json_response(self, load_version_info())
936
+ return
937
+
938
+ elif path == "/api/game_config":
939
+ send_json_response(self, load_game_config())
940
+ return
941
+
942
+ elif path == "/api/server_status":
943
+ cfg_data = load_game_config()
944
+ st = cfg_data.get("server_status", "OPEN").upper()
945
+ send_json_response(self, {
946
+ "status": st,
947
+ "is_open": (st == "OPEN"),
948
+ "emergency_alarm": GLOBAL_EMERGENCY_ALARM,
949
+ "broadcast_message": GLOBAL_BROADCAST.get("message", ""),
950
+ "broadcast_id": GLOBAL_BROADCAST.get("id", 0),
951
+ "ai_duel_trigger": GLOBAL_AI_DUEL_TRIGGER
952
+ })
953
+ return
954
+
955
+ elif path == "/api/devices":
956
+ if not self.check_router_access():
957
+ return
958
+ cfg_data = load_game_config()
959
+ dev_sec = cfg_data.get("device_security", {
960
+ "whitelist_enabled": True,
961
+ "allowed_devices": [],
962
+ "pending_devices": []
963
+ })
964
+ send_json_response(self, dev_sec)
965
+ return
966
+
967
+ elif path == "/api/verify_network":
968
+ client_ip = self.client_address[0]
969
+ forwarded = self.headers.get("X-Forwarded-For")
970
+ if forwarded:
971
+ client_ip = forwarded.split(",")[0].strip()
972
+ server_wan = get_wan_ip()
973
+ is_local = is_local_router_client(client_ip)
974
+ is_same_wan = (client_ip == server_wan)
975
+ allowed = is_local or is_same_wan
976
+ send_json_response(self, {
977
+ "allowed": allowed,
978
+ "client_ip": client_ip,
979
+ "server_wan": server_wan,
980
+ "is_local_router": is_local,
981
+ "network_match": allowed,
982
+ "message": "تم التحقق من شبكة الاتصال بنجاح" if allowed else "شبكة إنترنت غير مطابقة لشبكة السيرفر"
983
+ })
984
+ return
985
+
986
+ elif path == "/api/network_info":
987
+ if not self.check_router_access():
988
+ return
989
+ net_data = {
990
+ "lan_ip": get_lan_ip(),
991
+ "all_lan_ips": get_all_lan_ips(),
992
+ "wan_ip": get_wan_ip(),
993
+ "port": PORT,
994
+ "lan_url": f"http://{get_lan_ip()}:{PORT}",
995
+ "wan_url": f"http://{get_wan_ip()}:{PORT}",
996
+ "router_only_protection": True,
997
+ "status": "ONLINE"
998
+ }
999
+ send_json_response(self, net_data)
1000
+ return
1001
+
1002
+ elif path == "/api/rooms":
1003
+ import time
1004
+ now_ts = time.time()
1005
+ stale_keys = [k for k, v in ACTIVE_SESSIONS.items() if (now_ts - v.get("last_seen", 0)) > 180]
1006
+ for k in stale_keys:
1007
+ ACTIVE_SESSIONS.pop(k, None)
1008
+
1009
+ # Clean up stale rooms not seen in 90 seconds
1010
+ stale_rooms = [k for k, v in ROOMS.items() if (now_ts - v.get("last_seen", now_ts)) > 90]
1011
+ for k in stale_rooms:
1012
+ ROOMS.pop(k, None)
1013
+
1014
+ total_active = len(ROOMS) + len(ACTIVE_SESSIONS)
1015
+ total_players = len(ACTIVE_SESSIONS) + sum(max(len(r.get("players_list", [])), int(r.get("players", 1))) for r in ROOMS.values())
1016
+ send_json_response(self, {
1017
+ "total_created": max(TOTAL_ROOMS_CREATED, total_active),
1018
+ "active_count": total_active,
1019
+ "rooms": list(ROOMS.values()),
1020
+ "active_sessions": list(ACTIVE_SESSIONS.values()),
1021
+ "online_players_count": total_players,
1022
+ "emergency_alarm": GLOBAL_EMERGENCY_ALARM
1023
+ })
1024
+ return
1025
+
1026
+ elif path == "/api/rooms/check":
1027
+ code = urllib.parse.parse_qs(parsed.query).get("code", [""])[0]
1028
+ status_code, res = handle_rooms_check(code)
1029
+ send_json_response(self, res, status_code)
1030
+ return
1031
+
1032
+ elif path == "/api/console_logs":
1033
+ send_json_response(self, {"logs": CONSOLE_LOGS})
1034
+ return
1035
+
1036
+
1037
+
1038
+ elif path.startswith("/downloads/"):
1039
+ filename = path.replace("/downloads/", "").strip("/")
1040
+ file_path = DOWNLOADS_DIR / filename
1041
+ if file_path.is_file():
1042
+ file_size = file_path.stat().st_size
1043
+ self.send_response(200)
1044
+ if filename.lower().endswith(".ipa"):
1045
+ self.send_header("Content-Type", "application/octet-stream")
1046
+ else:
1047
+ self.send_header("Content-Type", "application/zip")
1048
+ self.send_header("Content-Length", str(file_size))
1049
+ self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
1050
+ self.send_header("Access-Control-Allow-Origin", "*")
1051
+ self.end_headers()
1052
+ with open(file_path, "rb") as f:
1053
+ while chunk := f.read(65536):
1054
+ self.wfile.write(chunk)
1055
+ return
1056
+ else:
1057
+ self.send_error(404, f"File {filename} not found")
1058
+ return
1059
+
1060
+ elif path in ("/guest", "/guest.html"):
1061
+ guest_file = PUBLIC_DIR / "guest.html"
1062
+ if guest_file.is_file():
1063
+ self.send_response(200)
1064
+ self.send_header("Content-Type", "text/html; charset=utf-8")
1065
+ self.send_header("Access-Control-Allow-Origin", "*")
1066
+ self.end_headers()
1067
+ with open(guest_file, "rb") as f:
1068
+ self.wfile.write(f.read())
1069
+ return
1070
+ else:
1071
+ self.send_error(404, "guest.html not found")
1072
+ return
1073
+
1074
+ elif path == "/" or path == "/index.html":
1075
+ if not self.check_router_access():
1076
+ return
1077
+ index_file = PUBLIC_DIR / "index.html"
1078
+ if index_file.is_file():
1079
+ self.send_response(200)
1080
+ self.send_header("Content-Type", "text/html; charset=utf-8")
1081
+ self.send_header("Access-Control-Allow-Origin", "*")
1082
+ self.end_headers()
1083
+ with open(index_file, "rb") as f:
1084
+ self.wfile.write(f.read())
1085
+ return
1086
+ else:
1087
+ self.send_error(404, "index.html not found")
1088
+ return
1089
+
1090
+ if not self.check_router_access():
1091
+ return
1092
+ super().do_GET()
1093
+
1094
+ def do_POST(self):
1095
+ parsed = urllib.parse.urlparse(self.path)
1096
+ path = parsed.path
1097
+
1098
+ if path == "/api/launch_game":
1099
+ global GLOBAL_AI_DUEL_TRIGGER
1100
+ GLOBAL_AI_DUEL_TRIGGER["active"] = True
1101
+ GLOBAL_AI_DUEL_TRIGGER["id"] = int(time.time() * 1000)
1102
+ GLOBAL_AI_DUEL_TRIGGER["room_code"] = "AI1V1"
1103
+ GLOBAL_AI_DUEL_TRIGGER["message"] = "⚔️ انطلقت مبارزة 1v1 مع CyberAI! جاري إدخالك للساحة..."
1104
+ try:
1105
+ import subprocess, psutil
1106
+ godot_running = any("godot" in p.name().lower() for p in psutil.process_iter(['name']))
1107
+ if not godot_running:
1108
+ godot_exe = r"C:\Users\Administrator\OneDrive\Desktop\Godot.exe"
1109
+ proj_dir = r"c:\Users\Administrator\OneDrive\Desktop\Game"
1110
+ subprocess.Popen([godot_exe, "--path", proj_dir])
1111
+ except Exception as ex:
1112
+ print("Launch game error:", ex)
1113
+ send_json_response(self, {"success": True, "message": "تم إرسال أمر فتح ودخول اللعبة بنجاح!"})
1114
+ return
1115
+
1116
+ elif path == "/api/sync_github_ipa":
1117
+ if not self.check_router_access():
1118
+ return
1119
+ try:
1120
+ from sync_github_ipa import sync_ipa_from_github_or_local
1121
+ result = sync_ipa_from_github_or_local()
1122
+ send_json_response(self, result)
1123
+ except Exception as e:
1124
+ self.send_error(500, f"Sync error: {e}")
1125
+ return
1126
+
1127
+ elif path == "/api/publish":
1128
+ if not self.check_router_access():
1129
+ return
1130
+ content_length = int(self.headers.get("Content-Length", 0))
1131
+ body = self.rfile.read(content_length)
1132
+ try:
1133
+ data = json.loads(body.decode("utf-8"))
1134
+ from publish_update import publish_update
1135
+ version = data.get("version", "1.1.3")
1136
+ version_code = int(data.get("version_code", 5))
1137
+ title = data.get("title", "تحديث جديد من لوحة التحكم")
1138
+ changelog = data.get("changelog", ["تحسينات عامة على النظام واللعبة"])
1139
+ mandatory = data.get("mandatory", False)
1140
+ updated_data = publish_update(version, version_code, title, changelog, mandatory)
1141
+ send_json_response(self, {"success": True, "message": "Version updated successfully!", "data": updated_data})
1142
+ except Exception as e:
1143
+ self.send_error(500, f"Publish error: {e}")
1144
+ return
1145
+
1146
+ elif path == "/api/game_config":
1147
+ if not self.check_router_access():
1148
+ return
1149
+ content_length = int(self.headers.get("Content-Length", 0))
1150
+ body = self.rfile.read(content_length)
1151
+ try:
1152
+ data = json.loads(body.decode("utf-8"))
1153
+ current_config = load_game_config()
1154
+ if "heroes" in data:
1155
+ current_config["heroes"] = data["heroes"]
1156
+ if "active_maps" in data:
1157
+ current_config["active_maps"] = data["active_maps"]
1158
+ if "server_announcement" in data:
1159
+ current_config["server_announcement"] = data["server_announcement"]
1160
+ if "server_status" in data:
1161
+ current_config["server_status"] = data["server_status"]
1162
+
1163
+ save_game_config(current_config)
1164
+ send_json_response(self, {
1165
+ "success": True,
1166
+ "message": "Game configuration updated successfully!",
1167
+ "config": current_config
1168
+ })
1169
+ except Exception as e:
1170
+ self.send_error(500, f"Game config error: {e}")
1171
+ return
1172
+
1173
+ elif path in ("/api/server_status", "/api/server_toggle"):
1174
+ if not self.check_router_access():
1175
+ return
1176
+ content_length = int(self.headers.get("Content-Length", 0))
1177
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1178
+ try:
1179
+ data = json.loads(body.decode("utf-8")) if body else {}
1180
+ current_config = load_game_config()
1181
+ current_st = current_config.get("server_status", "OPEN").upper()
1182
+ if "status" in data:
1183
+ new_st = str(data["status"]).strip().upper()
1184
+ else:
1185
+ new_st = "CLOSED" if current_st == "OPEN" else "OPEN"
1186
+
1187
+ current_config["server_status"] = new_st
1188
+ save_game_config(current_config)
1189
+
1190
+ send_json_response(self, {
1191
+ "success": True,
1192
+ "status": new_st,
1193
+ "is_open": (new_st == "OPEN"),
1194
+ "message": f"Server status updated to {new_st}"
1195
+ })
1196
+ except Exception as e:
1197
+ self.send_error(500, f"Server toggle error: {e}")
1198
+ return
1199
+
1200
+ elif path == "/api/shutdown":
1201
+ if not self.check_router_access():
1202
+ return
1203
+ send_json_response(self, {"success": True, "message": "Server process is stopping..."})
1204
+ def kill_soon():
1205
+ import time
1206
+ time.sleep(0.5)
1207
+ os._exit(0)
1208
+ threading.Thread(target=kill_soon, daemon=True).start()
1209
+ return
1210
+
1211
+ elif path == "/api/verify_device":
1212
+ content_length = int(self.headers.get("Content-Length", 0))
1213
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1214
+ try:
1215
+ data = json.loads(body.decode("utf-8")) if body else {}
1216
+ code, resp = handle_verify_device_request(data, self.client_address[0])
1217
+ self.send_response(code)
1218
+ self.send_header("Content-Type", "application/json; charset=utf-8")
1219
+ self.send_header("Access-Control-Allow-Origin", "*")
1220
+ self.end_headers()
1221
+ self.wfile.write(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
1222
+ return
1223
+ except Exception as e:
1224
+ self.send_error(500, f"Verify device error: {e}")
1225
+ return
1226
+
1227
+ elif path == "/api/request_device":
1228
+ content_length = int(self.headers.get("Content-Length", 0))
1229
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1230
+ try:
1231
+ data = json.loads(body.decode("utf-8")) if body else {}
1232
+ code, resp = handle_device_request_submission(data, self.client_address[0])
1233
+ self.send_response(code)
1234
+ self.send_header("Content-Type", "application/json; charset=utf-8")
1235
+ self.send_header("Access-Control-Allow-Origin", "*")
1236
+ self.end_headers()
1237
+ self.wfile.write(json.dumps(resp, ensure_ascii=False).encode("utf-8"))
1238
+ return
1239
+ except Exception as e:
1240
+ self.send_error(500, f"Request device error: {e}")
1241
+ return
1242
+
1243
+ elif path == "/api/device_action":
1244
+ if not self.check_router_access():
1245
+ return
1246
+ content_length = int(self.headers.get("Content-Length", 0))
1247
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1248
+ try:
1249
+ data = json.loads(body.decode("utf-8")) if body else {}
1250
+ action = data.get("action", "")
1251
+ target_id = str(data.get("device_id", "")).strip()
1252
+ custom_name = str(data.get("device_name", "")).strip()
1253
+
1254
+ current_config = load_game_config()
1255
+ dev_sec = current_config.get("device_security", {
1256
+ "whitelist_enabled": True,
1257
+ "allowed_devices": [],
1258
+ "pending_devices": []
1259
+ })
1260
+
1261
+ if action == "approve":
1262
+ clean_id = target_id.strip("{} ").lower()
1263
+ # Find in pending or create
1264
+ found_item = None
1265
+ new_pending = []
1266
+ for p in dev_sec.get("pending_devices", []):
1267
+ if p.get("id", "").strip("{} ").lower() == clean_id:
1268
+ found_item = p
1269
+ else:
1270
+ new_pending.append(p)
1271
+ dev_sec["pending_devices"] = new_pending
1272
+
1273
+ # Add to allowed
1274
+ dev_name = custom_name or (found_item.get("name") if found_item else "📱 هاتف مصرح له (Authorized Mobile)")
1275
+ allowed = dev_sec.get("allowed_devices", [])
1276
+ # Remove existing if any
1277
+ allowed = [a for a in allowed if a.get("id", "").strip("{} ").lower() != clean_id]
1278
+ allowed.append({
1279
+ "id": target_id,
1280
+ "name": dev_name,
1281
+ "status": "APPROVED",
1282
+ "approved_at": "2026-10-19"
1283
+ })
1284
+ dev_sec["allowed_devices"] = allowed
1285
+
1286
+ elif action == "remove":
1287
+ clean_id = target_id.strip("{} ").lower()
1288
+ dev_sec["allowed_devices"] = [
1289
+ a for a in dev_sec.get("allowed_devices", [])
1290
+ if a.get("id", "").strip("{} ").lower() != clean_id
1291
+ ]
1292
+
1293
+ elif action == "toggle_whitelist":
1294
+ new_val = not dev_sec.get("whitelist_enabled", True)
1295
+ dev_sec["whitelist_enabled"] = new_val
1296
+
1297
+ elif action == "add_manual":
1298
+ clean_id = target_id.strip("{} ").lower()
1299
+ allowed = dev_sec.get("allowed_devices", [])
1300
+ allowed = [a for a in allowed if a.get("id", "").strip("{} ").lower() != clean_id]
1301
+ allowed.append({
1302
+ "id": target_id,
1303
+ "name": custom_name or "📱 جهاز يدوي مصرح له",
1304
+ "status": "APPROVED",
1305
+ "approved_at": "2026-10-19"
1306
+ })
1307
+ dev_sec["allowed_devices"] = allowed
1308
+
1309
+ elif action == "approve_with_action":
1310
+ clean_id = target_id.strip("{} ").lower()
1311
+ selected_act = str(data.get("selected_action", "FULL_ACCESS")).upper()
1312
+
1313
+ found_item = None
1314
+ new_pending = []
1315
+ for p in dev_sec.get("pending_devices", []):
1316
+ if p.get("id", "").strip("{} ").lower() == clean_id:
1317
+ found_item = p
1318
+ else:
1319
+ new_pending.append(p)
1320
+ dev_sec["pending_devices"] = new_pending
1321
+
1322
+ base_name = custom_name or (found_item.get("name") if found_item else "📱 جهاز مصرح")
1323
+ allowed = dev_sec.get("allowed_devices", [])
1324
+ allowed = [a for a in allowed if a.get("id", "").strip("{} ").lower() != clean_id]
1325
+
1326
+ import datetime
1327
+ now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
1328
+
1329
+ if selected_act == "FULL_ACCESS":
1330
+ allowed.append({
1331
+ "id": target_id,
1332
+ "name": f"{base_name} 👑 (كامل الصلاحيات)",
1333
+ "status": "APPROVED",
1334
+ "role": "FULL_ACCESS",
1335
+ "approved_at": now_str
1336
+ })
1337
+ add_console_log("ADMIN", f"👑 وافق القائد على جهاز '{base_name}' ومنحه صلاحيات كاملة!", "SUCCESS")
1338
+ elif selected_act == "NORMAL_PLAYER":
1339
+ allowed.append({
1340
+ "id": target_id,
1341
+ "name": f"{base_name} 🎮 (لاعب عادي)",
1342
+ "status": "APPROVED",
1343
+ "role": "NORMAL_PLAYER",
1344
+ "approved_at": now_str
1345
+ })
1346
+ add_console_log("ADMIN", f"🎮 وافق القائد على جهاز '{base_name}' كلاعب عادي", "INFO")
1347
+ elif selected_act == "GIFT_COINS":
1348
+ allowed.append({
1349
+ "id": target_id,
1350
+ "name": f"{base_name} 🎁 (مكافأة 1000 عملة)",
1351
+ "status": "APPROVED",
1352
+ "role": "GIFT_COINS",
1353
+ "approved_at": now_str
1354
+ })
1355
+ add_console_log("ADMIN", f"🎁 وافق القائد على جهاز '{base_name}' وأهداه 1000 عملة ذهبية!", "SUCCESS")
1356
+ elif selected_act == "BLOCK":
1357
+ add_console_log("ADMIN", f"⛔ رفض وحظر القائد طلب الجهاز '{base_name}'", "ERROR")
1358
+
1359
+ dev_sec["allowed_devices"] = allowed
1360
+
1361
+ current_config["device_security"] = dev_sec
1362
+ save_game_config(current_config)
1363
+
1364
+ self.send_response(200)
1365
+ self.send_header("Content-Type", "application/json; charset=utf-8")
1366
+ self.send_header("Access-Control-Allow-Origin", "*")
1367
+ self.end_headers()
1368
+ self.wfile.write(json.dumps({
1369
+ "success": True,
1370
+ "action": action,
1371
+ "device_security": dev_sec,
1372
+ "message": "Device action completed successfully!"
1373
+ }, ensure_ascii=False).encode("utf-8"))
1374
+ return
1375
+ except Exception as e:
1376
+ self.send_error(500, f"Device action error: {e}")
1377
+ return
1378
+
1379
+ elif path == "/api/rooms/create":
1380
+ content_length = int(self.headers.get("Content-Length", 0))
1381
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1382
+ try:
1383
+ data = json.loads(body.decode("utf-8")) if body else {}
1384
+ code, resp = handle_rooms_create(data, self.client_address[0])
1385
+ send_json_response(self, resp, code)
1386
+ except Exception as e:
1387
+ self.send_error(500, f"Rooms create error: {e}")
1388
+ return
1389
+
1390
+ elif path == "/api/rooms/close":
1391
+ content_length = int(self.headers.get("Content-Length", 0))
1392
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1393
+ try:
1394
+ data = json.loads(body.decode("utf-8")) if body else {}
1395
+ code, resp = handle_rooms_close(data)
1396
+ send_json_response(self, resp, code)
1397
+ except Exception as e:
1398
+ self.send_error(500, f"Rooms close error: {e}")
1399
+ return
1400
+
1401
+ elif path == "/api/rooms/terminate":
1402
+ if not self.check_router_access():
1403
+ return
1404
+ content_length = int(self.headers.get("Content-Length", 0))
1405
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1406
+ try:
1407
+ data = json.loads(body.decode("utf-8")) if body else {}
1408
+ code, resp = handle_rooms_terminate(data)
1409
+ send_json_response(self, resp, code)
1410
+ except Exception as e:
1411
+ self.send_error(500, f"Rooms terminate error: {e}")
1412
+ return
1413
+
1414
+ elif path == "/api/rooms/message":
1415
+ if not self.check_router_access():
1416
+ return
1417
+ content_length = int(self.headers.get("Content-Length", 0))
1418
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1419
+ try:
1420
+ data = json.loads(body.decode("utf-8")) if body else {}
1421
+ code, resp = handle_rooms_message(data)
1422
+ send_json_response(self, resp, code)
1423
+ except Exception as e:
1424
+ self.send_error(500, f"Rooms message error: {e}")
1425
+ return
1426
+
1427
+ elif path == "/api/rooms/update":
1428
+ content_length = int(self.headers.get("Content-Length", 0))
1429
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1430
+ try:
1431
+ data = json.loads(body.decode("utf-8")) if body else {}
1432
+ code, resp = handle_rooms_update(data)
1433
+ send_json_response(self, resp, code)
1434
+ except Exception as e:
1435
+ self.send_error(500, f"Rooms update error: {e}")
1436
+ return
1437
+
1438
+ elif path == "/api/sessions/update":
1439
+ content_length = int(self.headers.get("Content-Length", 0))
1440
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1441
+ try:
1442
+ data = json.loads(body.decode("utf-8")) if body else {}
1443
+ code, resp = handle_session_update(data, self.client_address[0])
1444
+ send_json_response(self, resp, code)
1445
+ except Exception as e:
1446
+ self.send_error(500, f"Session update error: {e}")
1447
+ return
1448
+
1449
+ elif path == "/api/emergency_alarm":
1450
+ if not self.check_router_access():
1451
+ return
1452
+ content_length = int(self.headers.get("Content-Length", 0))
1453
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1454
+ try:
1455
+ import time
1456
+ data = json.loads(body.decode("utf-8")) if body else {}
1457
+ active = bool(data.get("active", True))
1458
+ msg = str(data.get("message", "🚨 إنذار طارئ من قائد السيرفر!")).strip()
1459
+ global GLOBAL_EMERGENCY_ALARM
1460
+ GLOBAL_EMERGENCY_ALARM = {
1461
+ "active": active,
1462
+ "message": msg,
1463
+ "id": int(time.time() * 1000)
1464
+ }
1465
+ status_txt = "تفعيل" if active else "إيقاف"
1466
+ add_console_log("ADMIN", f"🚨 قام القائد بـ ({status_txt}) صفارة الإنذار العامة باللعبة: '{msg}'", "WARNING" if active else "INFO")
1467
+ send_json_response(self, {"success": True, "emergency_alarm": GLOBAL_EMERGENCY_ALARM})
1468
+ except Exception as e:
1469
+ self.send_error(500, f"Alarm error: {e}")
1470
+ return
1471
+
1472
+ elif path == "/api/ai/chat":
1473
+ content_length = int(self.headers.get("Content-Length", 0))
1474
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1475
+ try:
1476
+ data = json.loads(body.decode("utf-8")) if body else {}
1477
+ prompt = str(data.get("prompt", "")).strip()
1478
+ ctx = str(data.get("context", "general")).strip()
1479
+ code, resp = call_gemini_ai(prompt, ctx)
1480
+ send_json_response(self, resp, code)
1481
+ except Exception as e:
1482
+ self.send_error(500, f"AI chat error: {e}")
1483
+ return
1484
+
1485
+ elif path == "/api/console_log":
1486
+ content_length = int(self.headers.get("Content-Length", 0))
1487
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1488
+ try:
1489
+ data = json.loads(body.decode("utf-8")) if body else {}
1490
+ msg = str(data.get("message", "")).strip()
1491
+ source = str(data.get("source", "GAME")).strip()
1492
+ lvl = str(data.get("level", "INFO")).strip()
1493
+ if msg:
1494
+ add_console_log(source, msg, lvl)
1495
+ self.send_response(200)
1496
+ self.send_header("Content-Type", "application/json; charset=utf-8")
1497
+ self.send_header("Access-Control-Allow-Origin", "*")
1498
+ self.end_headers()
1499
+ self.wfile.write(json.dumps({"success": True}).encode("utf-8"))
1500
+ except Exception as e:
1501
+ self.send_error(500, f"Console log error: {e}")
1502
+ return
1503
+
1504
+ elif path == "/api/console_clear":
1505
+ if not self.check_router_access():
1506
+ return
1507
+ CONSOLE_LOGS.clear()
1508
+ add_console_log("SYSTEM", "تم مسح سجلات الكونسول بواسطة القائد 🧹", "INFO")
1509
+ self.send_response(200)
1510
+ self.send_header("Content-Type", "application/json; charset=utf-8")
1511
+ self.send_header("Access-Control-Allow-Origin", "*")
1512
+ self.end_headers()
1513
+ self.wfile.write(json.dumps({"success": True, "message": "Console cleared"}).encode("utf-8"))
1514
+ return
1515
+
1516
+ self.send_error(404, "Endpoint not found")
1517
+
1518
+ def end_headers(self):
1519
+ self.send_header("Access-Control-Allow-Origin", "*")
1520
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
1521
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
1522
+ super().end_headers()
1523
+
1524
+ def do_OPTIONS(self):
1525
+ self.send_response(200)
1526
+ self.end_headers()
1527
+
1528
+ class GuestRequestHandler(http.server.SimpleHTTPRequestHandler):
1529
+ protocol_version = "HTTP/1.1"
1530
+
1531
+ """
1532
+ Dedicated Guest & Player handler (Port 8081):
1533
+ - Default page '/' serves guest.html
1534
+ - All admin POST endpoints (/api/publish, /api/game_config, /api/toggle_server, /api/shutdown, /api/device_action) are blocked (403 Forbidden)
1535
+ - Allows read-only APIs (/api/server_status, /api/game_config, /api/version) and downloads
1536
+ - Allows /api/request_device and /api/verify_device
1537
+ - Accessible from any client without router restriction
1538
+ """
1539
+ def __init__(self, *args, **kwargs):
1540
+ super().__init__(*args, directory=str(PUBLIC_DIR), **kwargs)
1541
+
1542
+ def do_GET(self):
1543
+ parsed = urllib.parse.urlparse(self.path)
1544
+ path = parsed.path
1545
+
1546
+ if path in ("/", "/index.html", "/guest", "/guest.html"):
1547
+ guest_file = PUBLIC_DIR / "guest.html"
1548
+ if guest_file.is_file():
1549
+ self.send_response(200)
1550
+ self.send_header("Content-Type", "text/html; charset=utf-8")
1551
+ self.send_header("Access-Control-Allow-Origin", "*")
1552
+ self.end_headers()
1553
+ with open(guest_file, "rb") as f:
1554
+ self.wfile.write(f.read())
1555
+ return
1556
+ else:
1557
+ self.send_error(404, "guest.html not found")
1558
+ return
1559
+
1560
+ elif path == "/api/server_status":
1561
+ cfg_data = load_game_config()
1562
+ st = cfg_data.get("server_status", "OPEN").upper()
1563
+ send_json_response(self, {
1564
+ "status": st,
1565
+ "is_open": (st == "OPEN"),
1566
+ "emergency_alarm": GLOBAL_EMERGENCY_ALARM,
1567
+ "broadcast_message": GLOBAL_BROADCAST.get("message", ""),
1568
+ "broadcast_id": GLOBAL_BROADCAST.get("id", 0),
1569
+ "ai_duel_trigger": GLOBAL_AI_DUEL_TRIGGER
1570
+ })
1571
+ return
1572
+
1573
+ elif path == "/api/game_config":
1574
+ send_json_response(self, load_game_config())
1575
+ return
1576
+
1577
+ elif path == "/api/version":
1578
+ send_json_response(self, load_version_info())
1579
+ return
1580
+
1581
+ elif path == "/api/rooms":
1582
+ import time
1583
+ now_ts = time.time()
1584
+ stale_keys = [k for k, v in ACTIVE_SESSIONS.items() if (now_ts - v.get("last_seen", 0)) > 180]
1585
+ for k in stale_keys:
1586
+ ACTIVE_SESSIONS.pop(k, None)
1587
+
1588
+ # Clean up stale rooms not seen in 90 seconds
1589
+ stale_rooms = [k for k, v in ROOMS.items() if (now_ts - v.get("last_seen", now_ts)) > 90]
1590
+ for k in stale_rooms:
1591
+ ROOMS.pop(k, None)
1592
+
1593
+ total_active = len(ROOMS) + len(ACTIVE_SESSIONS)
1594
+ total_players = len(ACTIVE_SESSIONS) + sum(max(len(r.get("players_list", [])), int(r.get("players", 1))) for r in ROOMS.values())
1595
+ send_json_response(self, {
1596
+ "total_created": max(TOTAL_ROOMS_CREATED, total_active),
1597
+ "active_count": total_active,
1598
+ "rooms": list(ROOMS.values()),
1599
+ "active_sessions": list(ACTIVE_SESSIONS.values()),
1600
+ "online_players_count": total_players,
1601
+ "emergency_alarm": GLOBAL_EMERGENCY_ALARM
1602
+ })
1603
+ return
1604
+
1605
+ elif path == "/api/rooms/check":
1606
+ code = urllib.parse.parse_qs(parsed.query).get("code", [""])[0]
1607
+ status_code, res = handle_rooms_check(code)
1608
+ send_json_response(self, res, status_code)
1609
+ return
1610
+
1611
+ elif path.startswith("/downloads/"):
1612
+ filename = path.replace("/downloads/", "").strip("/")
1613
+ file_path = DOWNLOADS_DIR / filename
1614
+ if file_path.is_file():
1615
+ file_size = file_path.stat().st_size
1616
+ self.send_response(200)
1617
+ if filename.lower().endswith(".ipa"):
1618
+ self.send_header("Content-Type", "application/octet-stream")
1619
+ else:
1620
+ self.send_header("Content-Type", "application/zip")
1621
+ self.send_header("Content-Length", str(file_size))
1622
+ self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
1623
+ self.send_header("Access-Control-Allow-Origin", "*")
1624
+ self.end_headers()
1625
+ with open(file_path, "rb") as f:
1626
+ while chunk := f.read(65536):
1627
+ self.wfile.write(chunk)
1628
+ return
1629
+ else:
1630
+ self.send_error(404, f"File {filename} not found")
1631
+ return
1632
+
1633
+ super().do_GET()
1634
+
1635
+ def do_POST(self):
1636
+ parsed = urllib.parse.urlparse(self.path)
1637
+ path = parsed.path
1638
+
1639
+ if path == "/api/request_device":
1640
+ content_length = int(self.headers.get("Content-Length", 0))
1641
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1642
+ try:
1643
+ data = json.loads(body.decode("utf-8")) if body else {}
1644
+ code, resp = handle_device_request_submission(data, self.client_address[0])
1645
+ send_json_response(self, resp, code)
1646
+ return
1647
+ except Exception as e:
1648
+ self.send_error(500, f"Request device error: {e}")
1649
+ return
1650
+
1651
+ elif path == "/api/verify_device":
1652
+ content_length = int(self.headers.get("Content-Length", 0))
1653
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1654
+ try:
1655
+ data = json.loads(body.decode("utf-8")) if body else {}
1656
+ code, resp = handle_verify_device_request(data, self.client_address[0])
1657
+ send_json_response(self, resp, code)
1658
+ return
1659
+ except Exception as e:
1660
+ self.send_error(500, f"Verify device error: {e}")
1661
+ return
1662
+
1663
+ elif path == "/api/rooms/create":
1664
+ content_length = int(self.headers.get("Content-Length", 0))
1665
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1666
+ try:
1667
+ data = json.loads(body.decode("utf-8")) if body else {}
1668
+ code, resp = handle_rooms_create(data, self.client_address[0])
1669
+ send_json_response(self, resp, code)
1670
+ except Exception as e:
1671
+ self.send_error(500, f"Rooms create error: {e}")
1672
+ return
1673
+
1674
+ elif path == "/api/rooms/update":
1675
+ content_length = int(self.headers.get("Content-Length", 0))
1676
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1677
+ try:
1678
+ data = json.loads(body.decode("utf-8")) if body else {}
1679
+ code, resp = handle_rooms_update(data)
1680
+ send_json_response(self, resp, code)
1681
+ except Exception as e:
1682
+ self.send_error(500, f"Rooms update error: {e}")
1683
+ return
1684
+
1685
+ elif path == "/api/sessions/update":
1686
+ content_length = int(self.headers.get("Content-Length", 0))
1687
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1688
+ try:
1689
+ data = json.loads(body.decode("utf-8")) if body else {}
1690
+ code, resp = handle_session_update(data, self.client_address[0])
1691
+ send_json_response(self, resp, code)
1692
+ except Exception as e:
1693
+ self.send_error(500, f"Session update error: {e}")
1694
+ return
1695
+
1696
+ elif path == "/api/rooms/close":
1697
+ content_length = int(self.headers.get("Content-Length", 0))
1698
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1699
+ try:
1700
+ data = json.loads(body.decode("utf-8")) if body else {}
1701
+ code, resp = handle_rooms_close(data)
1702
+ send_json_response(self, resp, code)
1703
+ except Exception as e:
1704
+ self.send_error(500, f"Rooms close error: {e}")
1705
+ return
1706
+
1707
+ elif path == "/api/console_log":
1708
+ content_length = int(self.headers.get("Content-Length", 0))
1709
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1710
+ try:
1711
+ data = json.loads(body.decode("utf-8")) if body else {}
1712
+ msg = str(data.get("message", "")).strip()
1713
+ source = str(data.get("source", "GAME")).strip()
1714
+ lvl = str(data.get("level", "INFO")).strip()
1715
+ if msg:
1716
+ add_console_log(source, msg, lvl)
1717
+ send_json_response(self, {"success": True})
1718
+ except Exception as e:
1719
+ self.send_error(500, f"Console log error: {e}")
1720
+ return
1721
+
1722
+ elif path == "/api/ai/chat":
1723
+ content_length = int(self.headers.get("Content-Length", 0))
1724
+ body = self.rfile.read(content_length) if content_length > 0 else b"{}"
1725
+ try:
1726
+ data = json.loads(body.decode("utf-8")) if body else {}
1727
+ prompt = str(data.get("prompt", "")).strip()
1728
+ ctx = str(data.get("context", "general")).strip()
1729
+ code, resp = call_gemini_ai(prompt, ctx)
1730
+ send_json_response(self, resp, code)
1731
+ except Exception as e:
1732
+ self.send_error(500, f"AI chat error: {e}")
1733
+ return
1734
+
1735
+ # Block any admin modification attempts on the guest server
1736
+ send_json_response(self, {
1737
+ "error": "Forbidden",
1738
+ "message": "⛔ هذا السيرفر مخصص للزوار واللاعبين فقط (Guest Only). التحكم الكامل متاح لمدير السيرفر فقط على البورت 8080."
1739
+ }, 403)
1740
+
1741
+ def end_headers(self):
1742
+ self.send_header("Access-Control-Allow-Origin", "*")
1743
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
1744
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
1745
+ super().end_headers()
1746
+
1747
+ def do_OPTIONS(self):
1748
+ self.send_response(200)
1749
+ self.end_headers()
1750
+
1751
+
1752
+ def run_cloud_keep_alive() -> None:
1753
+ """Keeps Render / cloud instance alive 24/7 by pinging it before the 15-minute idle sleep threshold."""
1754
+ target_url = os.environ.get("RENDER_EXTERNAL_URL", "").strip()
1755
+ if not target_url:
1756
+ target_url = os.environ.get("KEEP_ALIVE_URL", "").strip()
1757
+ if not target_url and IS_CLOUD:
1758
+ app_name = os.environ.get("RENDER_SERVICE_NAME", "titanstrike-server").strip()
1759
+ target_url = f"https://{app_name}.onrender.com"
1760
+
1761
+ if not target_url:
1762
+ return
1763
+
1764
+ target_url = target_url.rstrip("/") + "/api/server_status"
1765
+ print(f"[KeepAlive] 🕒 Starting 24/7 Cloud Keep-Alive pinger to: {target_url}")
1766
+
1767
+ time.sleep(25)
1768
+ while True:
1769
+ try:
1770
+ req = urllib.request.Request(target_url, headers={"User-Agent": "TitanStrike-KeepAlive/1.0"})
1771
+ with urllib.request.urlopen(req, timeout=15) as resp:
1772
+ if resp.status == 200:
1773
+ pass
1774
+ except Exception:
1775
+ pass
1776
+ # Sleep for 9 minutes (540s) so Render's 15-minute sleep timer is never reached!
1777
+ time.sleep(540)
1778
+
1779
+ def start_keep_alive_thread() -> threading.Thread:
1780
+ t = threading.Thread(target=run_cloud_keep_alive, daemon=True, name="KeepAliveAgent")
1781
+ t.start()
1782
+ return t
1783
+
1784
+ def run_server():
1785
+ lan_ip = get_lan_ip()
1786
+ wan_ip = get_wan_ip()
1787
+
1788
+ socketserver.TCPServer.allow_reuse_address = True
1789
+
1790
+ try:
1791
+ admin_httpd = socketserver.ThreadingTCPServer(("0.0.0.0", PORT), UpdateRequestHandler)
1792
+ except Exception as e:
1793
+ print(f"[!] Warning: Could not bind Admin server on port {PORT}: {e}")
1794
+ admin_httpd = None
1795
+
1796
+ try:
1797
+ if not IS_CLOUD:
1798
+ guest_httpd = socketserver.ThreadingTCPServer(("0.0.0.0", GUEST_PORT), GuestRequestHandler)
1799
+ else:
1800
+ guest_httpd = None
1801
+ except Exception as e:
1802
+ print(f"[!] Warning: Could not bind Guest server on port {GUEST_PORT}: {e}")
1803
+ guest_httpd = None
1804
+
1805
+ if not admin_httpd and not guest_httpd:
1806
+ print("[Error] Failed to start servers! Ports might be busy.")
1807
+ sys.exit(1)
1808
+
1809
+ print("=" * 75)
1810
+ print("⚡ CyberFort: Titan Strike 3D - Live Control & Guest Server System")
1811
+ print("=" * 75)
1812
+ if admin_httpd:
1813
+ admin_thread = threading.Thread(target=admin_httpd.serve_forever, daemon=True)
1814
+ admin_thread.start()
1815
+ print(f"👑 [Admin Server] (Port {PORT}) - لوحة التحكم الكاملة والمحمية للمدير:")
1816
+ print(f" - الرابط المحلي (Localhost): http://localhost:{PORT}")
1817
+ print(f" - رابط شبكة الواي فاي (Wi-Fi): http://{lan_ip}:{PORT}")
1818
+ print()
1819
+ if guest_httpd:
1820
+ guest_thread = threading.Thread(target=guest_httpd.serve_forever, daemon=True)
1821
+ guest_thread.start()
1822
+ print(f"👥 [Guest Server] (Port {GUEST_PORT}) - سيرفر وبوابة الزوار واللاعبين:")
1823
+ print(f" - رابط الزوار (Wi-Fi): http://{lan_ip}:{GUEST_PORT}")
1824
+ print(f" - رابط الإنترنت (WAN): http://{wan_ip}:{GUEST_PORT}")
1825
+ print("=" * 75)
1826
+ # Launch Telegram Bot Gemini Agent Daemon
1827
+ try:
1828
+ from telegram_bot import start_telegram_bot_thread
1829
+ start_telegram_bot_thread()
1830
+ print("🤖 [Telegram Bot Agent] نشط ويعمل بنجاح لاستقبال الأوامر وإرسال تنبيهات الأخطاء!")
1831
+ except Exception as e:
1832
+ print(f"[!] Warning: Could not start Telegram Bot: {e}")
1833
+
1834
+ # Launch Cloud 24/7 Keep-Alive Thread (Prevents Render 15-min Sleep)
1835
+ try:
1836
+ start_keep_alive_thread()
1837
+ except Exception as e:
1838
+ print(f"[!] Warning: Could not start Keep-Alive thread: {e}")
1839
+
1840
+ print("[*] All servers are running actively! Ready to serve clients.")
1841
+ print("[*] Press Ctrl+C anytime to stop.")
1842
+ print("=" * 75)
1843
+
1844
+ add_console_log("SYSTEM", "🚀 تم تشغيل سيرفر Titan Strike 3D بنجاح!", "SUCCESS")
1845
+ add_console_log("SYSTEM", f"👑 لوحة تحكم القائد نشطة على البورت {PORT}", "INFO")
1846
+ add_console_log("SYSTEM", f"👥 بوابة الزوار نشطة على البورت {GUEST_PORT}", "INFO")
1847
+
1848
+ try:
1849
+ while True:
1850
+ threading.Event().wait(3600)
1851
+ except KeyboardInterrupt:
1852
+ print("\n[*] Stopping all servers...")
1853
+ if admin_httpd:
1854
+ admin_httpd.server_close()
1855
+ if guest_httpd:
1856
+ guest_httpd.server_close()
1857
+
1858
+ if __name__ == "__main__":
1859
+ run_server()