File size: 19,138 Bytes
0789398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
"""
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<lang>\w+)?\n(?P<code>.*?)```",
    _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]}"