""" yui_code_runner.py — YUI CODE EXECUTION ENGINE v1.0 ===================================================== Hỗ trợ chạy code qua nhiều backend theo thứ tự ưu tiên: 1. PISTON API — Không cần key, 5 req/s, ~30+ ngôn ngữ (ưu tiên nhất) 2. JUDGE0 API — Cần key (RapidAPI free: 50/ngày), chính xác hơn 3. LOCAL SANDBOX — Python/JS/Bash local (fallback cuối) Rate limiting: - Guest/friend: 3 lần/phút, 20 lần/giờ - Admin (Bạch): không giới hạn Yui tự dùng khi detect code trong reply cần verify, hoặc user gọi /chaycode, hoặc AI tự test code mình vừa viết. """ import os, time, random, threading, asyncio from typing import Optional # ── RATE LIMITER ────────────────────────────────────────────────── _rate_lock = threading.Lock() _rate_data: dict = {} # {user_id: {"minute": [ts...], "hour": [ts...]}} RATE_LIMIT_MINUTE = 3 # requests / phút cho guest RATE_LIMIT_HOUR = 20 # requests / giờ cho guest def _check_rate(user_id: str, is_admin: bool = False) -> tuple[bool, str]: """ Kiểm tra rate limit. Returns: (allowed: bool, reason: str) """ if is_admin: return True, "" now = time.time() with _rate_lock: d = _rate_data.setdefault(user_id, {"minute": [], "hour": []}) # Dọn timestamps cũ d["minute"] = [t for t in d["minute"] if now - t < 60] d["hour"] = [t for t in d["hour"] if now - t < 3600] if len(d["minute"]) >= RATE_LIMIT_MINUTE: wait = int(60 - (now - d["minute"][0])) + 1 return False, f"⏳ Quá {RATE_LIMIT_MINUTE} lần/phút — chờ {wait}s nhé!" if len(d["hour"]) >= RATE_LIMIT_HOUR: wait = int(3600 - (now - d["hour"][0])) // 60 + 1 return False, f"⏳ Quá {RATE_LIMIT_HOUR} lần/giờ — chờ {wait} phút nhé!" d["minute"].append(now) d["hour"].append(now) return True, "" # ── LANGUAGE MAPPING ────────────────────────────────────────────── # Piston language slugs + versions PISTON_LANGS = { "python": ("python", "3.10.0"), "py": ("python", "3.10.0"), "python3": ("python", "3.10.0"), "javascript": ("javascript", "18.15.0"), "js": ("javascript", "18.15.0"), "node": ("javascript", "18.15.0"), "typescript": ("typescript", "5.0.3"), "ts": ("typescript", "5.0.3"), "java": ("java", "15.0.2"), "c": ("c", "10.2.0"), "cpp": ("c++", "10.2.0"), "c++": ("c++", "10.2.0"), "rust": ("rust", "1.50.0"), "go": ("go", "1.16.2"), "ruby": ("ruby", "3.0.1"), "php": ("php", "8.2.3"), "bash": ("bash", "5.2.0"), "sh": ("bash", "5.2.0"), "lua": ("lua", "5.4.4"), "kotlin": ("kotlin", "1.8.20"), "swift": ("swift", "5.8.0"), "r": ("r", "4.1.1"), "csharp": ("csharp", "6.12.0"), "cs": ("csharp", "6.12.0"), "perl": ("perl", "5.36.0"), } # Judge0 language IDs (RapidAPI) JUDGE0_LANG_IDS = { "python": 71, # Python 3.8.1 "py": 71, "python3": 71, "javascript": 63, # JavaScript (Node.js 12.14.0) "js": 63, "typescript": 74, # TypeScript 3.7.4 "ts": 74, "java": 62, # Java (OpenJDK 13.0.1) "c": 50, # C (GCC 9.2.0) "cpp": 54, # C++ (GCC 9.2.0) "c++": 54, "rust": 73, # Rust 1.40.0 "go": 60, # Go 1.13.5 "ruby": 72, # Ruby 2.7.0 "php": 68, # PHP 7.4.1 "bash": 46, # Bash 5.0.0 "csharp": 51, # C# Mono 6.6.0.161 "cs": 51, "r": 80, # R 4.0.0 "kotlin": 78, # Kotlin 1.3.70 "swift": 83, # Swift 5.2.3 } # ── SECURITY FILTER ─────────────────────────────────────────────── _BLOCKED_PATTERNS = [ "rm -rf", "del /f", "format c:", "shutdown", "reboot", "import os; os.system", "__import__('os')", "subprocess.call", "os.popen", "sys.exit", "socket.connect", "urllib.request.urlopen", "open('/etc/passwd')", "open('/etc/shadow')", ":(){:|:&};:", "fork bomb", "nc -e", "netcat", "mkfifo", ] def _security_check(code: str) -> tuple[bool, str]: """Kiểm tra code có chứa pattern nguy hiểm không.""" code_lower = code.lower() for pattern in _BLOCKED_PATTERNS: if pattern.lower() in code_lower: return False, f"🚫 Code chứa lệnh nguy hiểm (`{pattern}`) — bị chặn!" # Giới hạn độ dài if len(code) > 5000: return False, "🚫 Code quá dài (tối đa 5000 ký tự)" return True, "" # ── PISTON API ──────────────────────────────────────────────────── PISTON_URL = "https://emkc.org/api/v2/piston/execute" _piston_last_req = [0.0] _piston_lock = threading.Lock() async def _run_piston(code: str, lang: str, stdin: str = "") -> dict: """ Gọi Piston API — không cần key, 5 req/s rate limit. Returns: {"ok": bool, "output": str, "error": str, "time": float} """ lang_info = PISTON_LANGS.get(lang.lower()) if not lang_info: return {"ok": False, "output": "", "error": f"Piston không hỗ trợ ngôn ngữ: {lang}"} piston_lang, piston_ver = lang_info # Rate limit 5 req/s — dùng sleep nhẹ nếu cần with _piston_lock: now = time.time() gap = now - _piston_last_req[0] if gap < 0.22: # ~4.5 req/s để an toàn await asyncio.sleep(0.22 - gap) _piston_last_req[0] = time.time() payload = { "language": piston_lang, "version": piston_ver, "files": [{"name": "main", "content": code}], "stdin": stdin, "args": [], "compile_timeout": 10000, "run_timeout": 10000, "compile_memory_limit": -1, "run_memory_limit": -1, } try: import httpx t0 = time.time() async with httpx.AsyncClient(timeout=20) as client: resp = await client.post(PISTON_URL, json=payload) elapsed = round(time.time() - t0, 2) if resp.status_code == 429: return {"ok": False, "output": "", "error": "Piston rate limit (5/s) — thử lại sau 1s", "time": elapsed} if resp.status_code != 200: return {"ok": False, "output": "", "error": f"Piston HTTP {resp.status_code}", "time": elapsed} data = resp.json() run = data.get("run", {}) output = (run.get("stdout") or "").strip() stderr = (run.get("stderr") or "").strip() code_ret = run.get("code", 0) # Compile stage (C, Java, etc.) compile_info = data.get("compile", {}) if compile_info: cerr = (compile_info.get("stderr") or "").strip() if cerr and code_ret != 0: return {"ok": False, "output": output, "error": f"Compile error:\n{cerr[:800]}", "time": elapsed} if code_ret != 0 and not output: return {"ok": False, "output": "", "error": stderr[:800] or f"Exit code {code_ret}", "time": elapsed} return { "ok": True, "output": output[:1500], "error": stderr[:200] if stderr else "", "time": elapsed, "lang": f"{piston_lang} {piston_ver}", } except Exception as e: return {"ok": False, "output": "", "error": f"Piston exception: {str(e)[:100]}"} # ── JUDGE0 API ──────────────────────────────────────────────────── JUDGE0_URL = "https://judge0-ce.p.rapidapi.com" JUDGE0_ALT_URL = "https://judge0-extra-ce.p.rapidapi.com" # Extra languages JUDGE0_HOST = "judge0-ce.p.rapidapi.com" _judge0_daily = {} # {key_prefix: {"count": int, "reset_ts": float}} def _get_judge0_key() -> Optional[str]: """Lấy Judge0 key còn hạn ngạch từ env.""" keys = [] for k, v in os.environ.items(): if k.startswith("JUDGE0_KEY") or k.startswith("RAPIDAPI_KEY"): if v.strip(): keys.append(v.strip()) if not keys: return None now = time.time() for key in keys: prefix = key[:8] d = _judge0_daily.get(prefix, {"count": 0, "reset_ts": now + 86400}) if now > d["reset_ts"]: _judge0_daily[prefix] = {"count": 0, "reset_ts": now + 86400} return key if d["count"] < 45: # Buffer 5 để tránh hit giới hạn đột ngột return key return None # Tất cả key hết quota hôm nay def _mark_judge0_used(key: str): prefix = key[:8] d = _judge0_daily.get(prefix, {"count": 0, "reset_ts": time.time() + 86400}) d["count"] += 1 _judge0_daily[prefix] = d async def _run_judge0(code: str, lang: str, stdin: str = "") -> dict: """ Gọi Judge0 API — cần JUDGE0_KEY (RapidAPI free: 50/ngày). Chính xác hơn Piston, hỗ trợ nhiều ngôn ngữ hơn. """ key = _get_judge0_key() if not key: return {"ok": False, "output": "", "error": "Judge0: Hết quota hôm nay (50/ngày) — reset 00:00 UTC"} lang_id = JUDGE0_LANG_IDS.get(lang.lower()) if not lang_id: return {"ok": False, "output": "", "error": f"Judge0 không hỗ trợ: {lang}"} import base64, httpx payload = { "source_code": base64.b64encode(code.encode()).decode(), "language_id": lang_id, "stdin": base64.b64encode(stdin.encode()).decode() if stdin else "", "cpu_time_limit": 5, "memory_limit": 128000, } headers = { "X-RapidAPI-Key": key, "X-RapidAPI-Host": JUDGE0_HOST, "Content-Type": "application/json", } try: t0 = time.time() async with httpx.AsyncClient(timeout=30) as client: # Submit sub = await client.post( f"{JUDGE0_URL}/submissions?base64_encoded=true&wait=true", json=payload, headers=headers ) elapsed = round(time.time() - t0, 2) if sub.status_code == 429: return {"ok": False, "output": "", "error": "Judge0: 429 rate limit — thử lại sau"} if sub.status_code not in (200, 201): return {"ok": False, "output": "", "error": f"Judge0 HTTP {sub.status_code}"} _mark_judge0_used(key) result = sub.json() stdout = base64.b64decode(result.get("stdout") or "").decode(errors="replace").strip() stderr = base64.b64decode(result.get("stderr") or "").decode(errors="replace").strip() compile_out = base64.b64decode(result.get("compile_output") or "").decode(errors="replace").strip() status = result.get("status", {}).get("description", "") time_ms = result.get("time", "?") if status == "Accepted": return {"ok": True, "output": stdout[:1500], "error": "", "time": elapsed, "exec_time": time_ms} elif compile_out: return {"ok": False, "output": "", "error": f"Compile error:\n{compile_out[:800]}", "time": elapsed} else: err = stderr or f"Status: {status}" return {"ok": False, "output": stdout[:500], "error": err[:500], "time": elapsed} except Exception as e: return {"ok": False, "output": "", "error": f"Judge0 exception: {str(e)[:100]}"} # ── LOCAL SANDBOX ───────────────────────────────────────────────── async def _run_local(code: str, lang: str) -> dict: """Fallback: chạy local (chỉ Python, JS, Bash trên HF Space).""" import subprocess, tempfile, os as _os lang_map = { "python": ("python3", ".py"), "py": ("python3", ".py"), "python3": ("python3", ".py"), "javascript": ("node", ".js"), "js": ("node", ".js"), "bash": ("bash", ".sh"), "sh": ("bash", ".sh"), } if lang.lower() not in lang_map: return {"ok": False, "output": "", "error": f"Local sandbox chỉ hỗ trợ Python/JS/Bash"} runner, ext = lang_map[lang.lower()] t0 = time.time() with tempfile.NamedTemporaryFile(mode="w", suffix=ext, delete=False, encoding="utf-8") as f: f.write(code); tmp = f.name try: res = subprocess.run( [runner, tmp], capture_output=True, text=True, timeout=10, env={"PATH": "/usr/bin:/bin:/usr/local/bin", "HOME": "/tmp"} ) elapsed = round(time.time() - t0, 2) out = res.stdout.strip()[:1500] err = res.stderr.strip()[:400] if res.returncode == 0: return {"ok": True, "output": out, "error": err, "time": elapsed, "source": "local"} return {"ok": False, "output": out, "error": err or f"Exit {res.returncode}", "time": elapsed, "source": "local"} except subprocess.TimeoutExpired: return {"ok": False, "output": "", "error": "Timeout (>10s)", "time": 10.0, "source": "local"} except Exception as e: return {"ok": False, "output": "", "error": str(e)[:80], "source": "local"} finally: try: _os.unlink(tmp) except: pass # ── MAIN ENTRY POINT ────────────────────────────────────────────── async def run_code( code: str, lang: str = "python", stdin: str = "", user_id: str = "unknown", is_admin: bool = False, source: str = "manual", # "manual"|"auto" (Yui tự gọi) ) -> dict: """ Chạy code với fallback chain: Piston → Judge0 → Local Returns: { "ok": bool, "output": str, "error": str, "time": float, "backend": str, # "piston"|"judge0"|"local" "rate_blocked": bool, "rate_msg": str, } """ # Rate limit (auto-calls từ Yui không bị limit khi is_admin) if source == "manual": allowed, rate_msg = _check_rate(user_id, is_admin) if not allowed: return {"ok": False, "output": "", "error": rate_msg, "rate_blocked": True, "rate_msg": rate_msg, "backend": "none"} # Security check safe, reason = _security_check(code) if not safe: return {"ok": False, "output": "", "error": reason, "rate_blocked": False, "backend": "none"} lang = lang.lower().strip() # 1. Thử Piston (không cần key) result = await _run_piston(code, lang, stdin) if result["ok"]: result["backend"] = "piston" return result piston_err = result.get("error", "") print(f"[CODE-RUN] Piston fail ({piston_err[:50]}) → Judge0") # 2. Thử Judge0 (cần key, 50/ngày) if _get_judge0_key(): result = await _run_judge0(code, lang, stdin) if result["ok"]: result["backend"] = "judge0" return result judge0_err = result.get("error", "") print(f"[CODE-RUN] Judge0 fail ({judge0_err[:50]}) → local") else: judge0_err = "Không có Judge0 key" # 3. Local sandbox (fallback cuối) result = await _run_local(code, lang) result["backend"] = result.get("source", "local") return result def format_run_result(result: dict, lang: str = "", code_preview: bool = True) -> str: """Format kết quả chạy code thành string đẹp cho Discord.""" if result.get("rate_blocked"): return result["rate_msg"] backend = result.get("backend", "?") elapsed = result.get("time", "?") exec_t = result.get("exec_time", "") ok = result.get("ok", False) output = result.get("output", "").strip() error = result.get("error", "").strip() icon = "✅" if ok else "❌" time_str = f"{elapsed}s" if elapsed != "?" else "" if exec_t: time_str += f" (exec: {exec_t}s)" lines = [f"{icon} **[{backend.upper()}]** {lang} • {time_str}"] if ok and output: if len(output) > 1000: output = output[:1000] + "\n... (truncated)" lines.append(f"```\n{output}\n```") elif ok and not output: lines.append("*(Không có output)*") else: lines.append(f"```\n{error or 'Lỗi không xác định'}\n```") if output: lines.append(f"stdout: `{output[:200]}`") return "\n".join(lines) # ── YUI AUTO CODE TEST ──────────────────────────────────────────── import re as _re_coderun _CODE_BLOCK_RE = _re_coderun.compile( r"```(?P\w+)?\n(?P.*?)```", _re_coderun.DOTALL ) _AUTO_TEST_LANGS = {"python", "py", "javascript", "js", "typescript", "ts"} _AUTO_TEST_TRIGGERS = [ "def ", "function ", "const ", "let ", "var ", "class ", "import ", "from ", "print(", "console.log(", "return ", "async def ", "async function", ] def should_auto_test(reply: str, task_type: str) -> Optional[tuple[str, str]]: """ Kiểm tra xem reply của Yui có code block đáng test không. Returns: (code, lang) hoặc None """ if task_type not in ("code", "debug", "math"): return None match = _CODE_BLOCK_RE.search(reply) if not match: return None lang = (match.group("lang") or "python").lower().strip() code = match.group("code").strip() if lang not in _AUTO_TEST_LANGS: return None if len(code) < 10: return None if not any(t in code for t in _AUTO_TEST_TRIGGERS): return None return code, lang async def auto_test_reply(reply: str, task_type: str, timeout: int = 8) -> Optional[str]: """ Yui tự test code trong reply của mình. Nếu có lỗi → báo ngắn gọn để Yui tự sửa. Returns: None (OK) hoặc error string """ info = should_auto_test(reply, task_type) if not info: return None code, lang = info try: result = await asyncio.wait_for( run_code(code, lang, user_id="yui_auto", is_admin=True, source="auto"), timeout=timeout ) if not result["ok"]: err = result.get("error", "unknown error") return f"[AUTO-TEST] Code lỗi: {err[:200]}" return None # OK except asyncio.TimeoutError: return "[AUTO-TEST] Timeout khi test code" except Exception as e: return f"[AUTO-TEST] Exception: {str(e)[:80]}"