import os import subprocess import shutil import base64 import struct import zlib import pickle import sys import traceback from flask import Flask, request, jsonify from decompiler import magic, renpycompat sys.setrecursionlimit(50000) app = Flask(__name__) # --- 1. YARDIMCI VE ZIRHLI ÇEVİRİ FONKSİYONLARI --- def unescape_wp_string(s): if not isinstance(s, str): return s s = s.replace('\\n', '\n') s = s.replace('\\"', '"') s = s.replace('\\ ', ' ') s = s.replace('\\\\', '\\') return s.replace('\r', '') def apply_translation(text, translations): """AST Zırhı: Sadece birebir eşleşen metinleri değiştirir.""" if not isinstance(text, (str, bytes)): return text is_bytes = isinstance(text, bytes) text_str = text.decode('utf-8', 'ignore') if is_bytes else text for k, v in translations.items(): clean_k = k[1:-1] if len(k) >= 2 and k[0] in ('"', "'") and k[-1] == k[0] else k clean_v = v[1:-1] if len(v) >= 2 and v[0] in ('"', "'") and v[-1] == v[0] else v # Görünmez \r karakterlerini yoksayarak eşleşme oranını artır if text_str.replace('\r', '') == clean_k.replace('\r', ''): return clean_v.encode('utf-8') if is_bytes else clean_v return text # --- 2. AST MOTORU --- def patch_ast(obj, translations, visited=None): if visited is None: visited = set() # FIX: str ve bytes'ı burada durdurmuyoruz; çağıran zaten kontrol ediyor if obj is None or isinstance(obj, (int, float, bool)): return obj_id = id(obj) if obj_id in visited: return visited.add(obj_id) if isinstance(obj, (str, bytes)): # Tek başına gelen string'lere yapacak bir şey yok (referansı değiştiremeyiz) return if isinstance(obj, list): for i in range(len(obj)): if isinstance(obj[i], (str, bytes)): obj[i] = apply_translation(obj[i], translations) else: patch_ast(obj[i], translations, visited) elif isinstance(obj, tuple): for item in obj: patch_ast(item, translations, visited) elif isinstance(obj, dict): for k, v in list(obj.items()): if isinstance(v, (str, bytes)): obj[k] = apply_translation(v, translations) else: patch_ast(v, translations, visited) elif hasattr(obj, '__dict__'): class_name = type(obj).__name__ # --- Ren'Py Sınıflarına Göre Hedefli Yama --- # FIX: 'Translate' bloğu (translate turkish xxx: ... ) işleniyor # Bu sınıf, 'block' listesi içinde Say objelerini barındırır if class_name == 'Translate' and hasattr(obj, 'block'): patch_ast(obj.block, translations, visited) # identifier, language gibi meta alanları atla, devam et TRANSLATE_SKIP = {'old', 'language', 'identifier', 'filename', 'name', 'label', 'parameters', 'block'} for k, v in list(obj.__dict__.items()): if k in TRANSLATE_SKIP: continue if isinstance(v, (str, bytes)): if len(v if isinstance(v, str) else v.decode('utf-8', 'ignore')) >= 2: setattr(obj, k, apply_translation(v, translations)) else: patch_ast(v, translations, visited) return # Translate için işlem tamam if class_name in ('Say', 'TranslateSay') and hasattr(obj, 'what'): obj.what = apply_translation(obj.what, translations) elif class_name in ('Menu', 'TranslateMenu') and hasattr(obj, 'items'): new_items = [] for item in obj.items: label = apply_translation(item[0], translations) new_items.append((label, item[1], item[2]) if len(item) >= 3 else (label,) + item[1:]) obj.items = new_items elif class_name == 'TranslateString' and hasattr(obj, 'new'): obj.new = apply_translation(obj.new, translations) FORBIDDEN_KEYS = {'old', 'language', 'identifier', 'filename', 'name', 'label', 'parameters'} for k, v in list(obj.__dict__.items()): if k in FORBIDDEN_KEYS: continue if class_name in ('Say', 'TranslateSay') and k == 'what': continue if class_name in ('Menu', 'TranslateMenu') and k == 'items': continue if class_name == 'TranslateString' and k == 'new': continue if isinstance(v, (str, bytes)): text_len = len(v) if isinstance(v, str) else len(v.decode('utf-8', 'ignore')) if text_len >= 2: setattr(obj, k, apply_translation(v, translations)) else: patch_ast(v, translations, visited) # --- 3. ENDPOINT'LER --- @app.route('/decompile', methods=['POST']) def decompile_rpyc(): req_data = request.json if not req_data or 'filedata' not in req_data: return jsonify({"error": "Dosya verisi bulunamadı."}), 400 try: file_data = base64.b64decode(req_data['filedata']) filename = req_data.get('filename', 'temp.rpyc') rpy_filename = filename.replace('.rpyc', '.rpy') with open(filename, 'wb') as f: f.write(file_data) subprocess.run(['python', 'unrpyc.py', filename], check=True) with open(rpy_filename, 'r', encoding='utf-8') as f: rpy_content = f.read() if os.path.exists(filename): os.remove(filename) if os.path.exists(rpy_filename): os.remove(rpy_filename) return jsonify({"success": True, "rpy_content": rpy_content}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route('/patch', methods=['POST']) def patch_endpoint(): # FIX: Değişken adı 'req_data' olarak değiştirildi — loop içindeki 'data' ile çakışmayı önler req_data = request.get_json() if not req_data or 'filedata' not in req_data or 'translations' not in req_data: return jsonify({"error": "Eksik veri gönderildi."}), 400 try: file_bytes = base64.b64decode(req_data['filedata']) raw_translations = req_data['translations'] clean_translations = {} for k, v in raw_translations.items(): clean_k = unescape_wp_string(k) clean_v = unescape_wp_string(v) if clean_k.strip() and clean_k != clean_v: clean_translations[clean_k] = clean_v if file_bytes.startswith(b"RENPY RPC2"): position = 10 chunks = [] while True: slot, start, length = struct.unpack("III", file_bytes[position:position+12]) position += 12 if slot == 0: break chunks.append({"slot": slot, "start": start, "length": length}) payloads = {} for c in chunks: chunk_data = file_bytes[c["start"] : c["start"] + c["length"]] if c["slot"] == 1: raw_pickle = zlib.decompress(chunk_data) orig_proto = 2 if len(raw_pickle) >= 2 and raw_pickle[0] == 0x80: orig_proto = raw_pickle[1] ast_tree = renpycompat.pickle_loads(raw_pickle) patch_ast(ast_tree, clean_translations) payloads[1] = zlib.compress(pickle.dumps(ast_tree, protocol=orig_proto)) elif c["slot"] == 2: try: raw_source = zlib.decompress(chunk_data).decode('utf-8') for k, v in clean_translations.items(): ck = k[1:-1] if len(k) >= 2 and k[0] in ('"', "'") and k[-1] == k[0] else k cv = v[1:-1] if len(v) >= 2 and v[0] in ('"', "'") and v[-1] == v[0] else v raw_source = raw_source.replace(f'"{ck}"', f'"{cv}"') raw_source = raw_source.replace(f"'{ck}'", f"'{cv}'") # FIX: Kaçış karakterli tırnak varyantlarını da yakala raw_source = raw_source.replace(f'\\"{ ck}\\"', f'\\"{cv}\\"') payloads[2] = zlib.compress(raw_source.encode('utf-8')) except Exception: payloads[2] = chunk_data else: payloads[c["slot"]] = chunk_data new_dir = bytearray() current_offset = 10 + (len(chunks) + 1) * 12 for c in chunks: # FIX: 'payload_data' kullanıldı — üstteki req_data ile karışmaz payload_data = payloads[c["slot"]] new_dir.extend(struct.pack("III", c["slot"], current_offset, len(payload_data))) current_offset += len(payload_data) new_dir.extend(struct.pack("III", 0, 0, 0)) new_file = bytearray(b"RENPY RPC2") new_file.extend(new_dir) for c in chunks: new_file.extend(payloads[c["slot"]]) new_rpyc_bytes = bytes(new_file) else: raw_pickle = zlib.decompress(file_bytes) orig_proto = 2 if len(raw_pickle) >= 2 and raw_pickle[0] == 0x80: orig_proto = raw_pickle[1] ast_tree = renpycompat.pickle_loads(raw_pickle) patch_ast(ast_tree, clean_translations) new_rpyc_bytes = zlib.compress(pickle.dumps(ast_tree, protocol=orig_proto)) return jsonify({ 'success': True, 'patched_file': base64.b64encode(new_rpyc_bytes).decode('utf-8') }) except Exception as e: error_msg = traceback.format_exc() print("PATCH ERROR:", error_msg) return jsonify({'error': str(e)}), 500 if __name__ == '__main__': port = int(os.environ.get("PORT", 7860)) app.run(host='0.0.0.0', port=port)