commit b6c058c40f61fcba3508e9731dbf2eaaafd571fc Author: Radiquum Date: Sun Aug 31 19:51:58 2025 +0500 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9079f6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.python-venv +.venv +apks +original +tools +decompiled +dist +__pycache__ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..976544c --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.7 diff --git a/config.json b/config.json new file mode 100644 index 0000000..6337eae --- /dev/null +++ b/config.json @@ -0,0 +1,15 @@ +{ + "tools": [ + { + "tool": "apktool.jar", + "url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar" + } + ], + "folders": { + "tools": "./tools", + "apks": "./apks", + "decompiled": "./decompiled", + "patches": "./patches", + "dist": "./dist" + } +} \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..a6c4912 --- /dev/null +++ b/config.py @@ -0,0 +1,47 @@ +import json +import logging +import os +from typing import TypedDict +from rich.logging import RichHandler +from rich.console import Console + + +FORMAT = "%(message)s" +logging.basicConfig( + level="NOTSET", + format=FORMAT, + datefmt="[%X]", + handlers=[RichHandler(rich_tracebacks=True)], +) +log = logging.getLogger("rich") +console = Console() + + +class ConfigTools(TypedDict): + tool: str + url: str + + +class ConfigFolders(TypedDict): + tools: str + apks: str + decompiled: str + patches: str + dist: str + + +class Config(TypedDict): + tools: list[ConfigTools] + folders: ConfigFolders + + +def load_config() -> Config: + if not os.path.exists("config.json"): + log.exception("file `config.json` is not found!") + exit(1) + + with open("./config.json", "r", encoding="utf-8") as file: + return json.loads(file.read()) + + +config = load_config() diff --git a/main.py b/main.py new file mode 100644 index 0000000..668d022 --- /dev/null +++ b/main.py @@ -0,0 +1,29 @@ +from scripts.download_tools import check_and_download_all_tools +from scripts.select_apk import get_apks, select_apk +from scripts.select_patches import apply_patches, get_patches, select_patches +from scripts.utils import check_java_version, decompile_apk +from config import log, console + + +if __name__ == "__main__": + check_and_download_all_tools() + check_java_version() + + apks = get_apks() + if not apks: + log.fatal(f"apks folder is empty") + exit(1) + + apk = select_apk(apks) + decompile_apk(apk) + + patches = get_patches() + patches = select_patches(patches) + + statuses = apply_patches(patches) + + for status in statuses: + if status["status"]: + console.print(f"{status['name']}: ✔", style="bold green") + else: + console.print(f"{status['name']}: ✘", style="bold red") diff --git a/patches/__init__.py b/patches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/patches/change_package_name.config.json b/patches/change_package_name.config.json new file mode 100644 index 0000000..56b2452 --- /dev/null +++ b/patches/change_package_name.config.json @@ -0,0 +1,3 @@ +{ + "new_package_name": "com.radiquum.anixart" +} \ No newline at end of file diff --git a/patches/change_package_name.py b/patches/change_package_name.py new file mode 100644 index 0000000..16d8063 --- /dev/null +++ b/patches/change_package_name.py @@ -0,0 +1,61 @@ +"""Remove and Compress unnecessary resources""" + +priority = -10 +from tqdm import tqdm + +import os +import shutil +from typing import TypedDict + +class PatchConfig_ChangePackageName(TypedDict): + src: str + new_package_name: str + +def rename_dir(src, dst): + os.makedirs(dst, exist_ok=True) + os.rename(src, dst) + +def apply(config: dict) -> bool: + assert config["new_package_name"] is not None, "new_package_name is not configured" + + for root, dirs, files in os.walk(f"{config['src']}"): + for filename in files: + file_path = os.path.join(root, filename) + + if os.path.isfile(file_path): + try: + with open(file_path, "r", encoding="utf-8") as file: + file_contents = file.read() + + new_contents = file_contents.replace( + "com.swiftsoft.anixartd", config["new_package_name"] + ) + new_contents = new_contents.replace( + "com/swiftsoft/anixartd", + config["new_package_name"].replace(".", "/"), + ) + + with open(file_path, "w", encoding="utf-8") as file: + file.write(new_contents) + except: + pass + + if os.path.exists(f"{config['src']}/smali/com/swiftsoft/anixartd"): + rename_dir( + f"{config['src']}/smali/com/swiftsoft/anixartd", + os.path.join( + f"{config['src']}", "smali", config["new_package_name"].replace(".", "/") + ), + ) + + if os.path.exists(f"{config['src']}/smali_classes2/com/swiftsoft/anixartd"): + rename_dir( + f"{config['src']}/smali_classes2/com/swiftsoft/anixartd", + os.path.join( + f"{config['src']}", + "smali_classes2", + config["new_package_name"].replace(".", "/"), + ), + ) + + return True \ No newline at end of file diff --git a/patches/compress.config.json b/patches/compress.config.json new file mode 100644 index 0000000..645e742 --- /dev/null +++ b/patches/compress.config.json @@ -0,0 +1,3 @@ +{ + "keep_dirs": ["META-INF", "kotlin"] +} \ No newline at end of file diff --git a/patches/compress.py b/patches/compress.py new file mode 100644 index 0000000..1be4d0e --- /dev/null +++ b/patches/compress.py @@ -0,0 +1,25 @@ +"""Remove and Compress unnecessary resources""" + +priority = 0 +from tqdm import tqdm + +import os +import shutil +from typing import TypedDict + +class PatchConfig_Compress(TypedDict): + src: str + keep_dirs: list[str] + +def apply(config: PatchConfig_Compress) -> bool: + for item in os.listdir(f"{config['src']}/unknown/"): + item_path = os.path.join(f"{config['src']}/unknown/", item) + + if os.path.isfile(item_path): + os.remove(item_path) + tqdm.write(f"removed file: {item_path}") + elif os.path.isdir(item_path): + if item not in config["keep_dirs"]: + shutil.rmtree(item_path) + tqdm.write(f"removed directory: {item_path}") + return True diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05f639f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +requests +tqdm +lxml +rich +beaupy \ No newline at end of file diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/download_tools.py b/scripts/download_tools.py new file mode 100644 index 0000000..9eb4ec9 --- /dev/null +++ b/scripts/download_tools.py @@ -0,0 +1,46 @@ +import requests +import os +from tqdm import tqdm +from config import config, log + + +def check_if_tool_exists(tool: str) -> bool: + if not os.path.exists(config["folders"]["tools"]): + log.info(f"creating `tools` folder: {config['folders']['tools']}") + os.mkdir(config["folders"]["tools"]) + + if not os.path.exists(f"{config['folders']['tools']}/{tool}"): + return False + elif os.path.exists(f"{config['folders']['tools']}/{tool}") and os.path.isdir( + f"{config['folders']['tools']}/{tool}" + ): + log.warning(f"`{config['folders']['tools']}/{tool}` is a folder") + return True + else: + return True + + +def download_tool(url: str, tool: str): + if not check_if_tool_exists(tool): + log.info(f"downloading a tool: `{tool}`") + try: + response = requests.get(url, stream=True) + total = int(response.headers.get("content-length", 0)) + with open(f"{config['folders']['tools']}/{tool}", "wb") as file, tqdm( + desc=tool, + total=total, + unit="iB", + unit_scale=True, + unit_divisor=1024, + ) as bar: + for bytes in response.iter_content(chunk_size=8192): + size = file.write(bytes) + bar.update(size) + log.info(f"`{tool}` downloaded") + except Exception as e: + log.error(f"error while downloading `{tool}`: {e}") + + +def check_and_download_all_tools(): + for tool in config["tools"]: + download_tool(tool["url"], tool["tool"]) diff --git a/scripts/select_apk.py b/scripts/select_apk.py new file mode 100644 index 0000000..430d9de --- /dev/null +++ b/scripts/select_apk.py @@ -0,0 +1,27 @@ +import os +from beaupy import select +from config import config, log, console + +def get_apks() -> list[str]: + apks = [] + if not os.path.exists(config["folders"]["apks"]): + log.info(f"creating `apks` folder: {config['folders']['apks']}") + os.mkdir(config["folders"]["apks"]) + return apks + + for file in os.listdir(config["folders"]["apks"]): + if file.endswith(".apk") and os.path.isfile(f"{config['folders']['apks']}/{file}"): + apks.append(file) + + return apks + +def select_apk(apks: list[str]) -> str: + console.print("select apk file to patch") + apks.append("cancel") + + apk = select(apks, cursor="->", cursor_style="cyan") + if apk == "cancel": + log.info("patching cancelled") + exit(0) + + return apk \ No newline at end of file diff --git a/scripts/select_patches.py b/scripts/select_patches.py new file mode 100644 index 0000000..c86fed5 --- /dev/null +++ b/scripts/select_patches.py @@ -0,0 +1,87 @@ +import os, json +import importlib +from typing import TypedDict +from beaupy import select_multiple +from tqdm import tqdm +from config import config, log, console + + +class Patch: + def __init__(self, name, pkg): + self.name = name + self.package = pkg + self.applied = False + try: + self.priority = pkg.priority + except AttributeError: + self.priority = 0 + + def apply(self, conf: dict) -> bool: + try: + self.applied = self.package.apply(conf) + return True + except Exception as e: + log.error( + f"error while applying a patch {self.name}: %s, with args: %s", + e, + e.args, + exc_info=True, + ) + return False + + +def get_patches() -> list[str]: + patches = [] + if not os.path.exists(config["folders"]["patches"]): + log.info(f"creating `patches` folder: {config['folders']['patches']}") + os.mkdir(config["folders"]["patches"]) + return patches + + for file in os.listdir(config["folders"]["patches"]): + if ( + file.endswith(".py") + and os.path.isfile(f"{config['folders']['patches']}/{file}") + and file != "__init__.py" + ): + patches.append(file[:-3]) + + return patches + + +def select_patches(patches: list[str]) -> list[str]: + console.print("select patches to apply") + applied = select_multiple(patches, tick_character="X") + return applied + + +class PatchStatus(TypedDict): + name: str + status: bool + + +def apply_patches(patches: list[str]) -> list[PatchStatus]: + modules = [] + statuses = [] + + for name in patches: + module = importlib.import_module( + f"{config['folders']['patches'].removeprefix("./")}.{name}" + ) + modules.append(Patch(name, module)) + modules.sort(key=lambda x: x.package.priority, reverse=True) + + for patch in tqdm(modules, colour="green", desc="patching apk"): + tqdm.write(f"patch apply: {patch.name}") + conf = {} + if os.path.exists(f"{config['folders']['patches']}/{patch.name}.config.json"): + with open( + f"{config['folders']['patches']}/{patch.name}.config.json", + "r", + encoding="utf-8", + ) as conf: + conf = json.loads(conf.read()) + conf["src"] = config["folders"]["decompiled"] + status = patch.apply(conf) + statuses.append({"name": patch.name, "status": status}) + + return statuses diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 0000000..c8c3520 --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,43 @@ +import os +import shutil +import subprocess +from config import log, config + + +def check_java_version(): + try: + result = subprocess.run( + ["java", "-version"], capture_output=True, text=True, check=True + ) + version_line = result.stderr.splitlines()[0] + if not any(f"{i}." in version_line for i in range(9, 100)): + log.error(f"java 8+ is not installed") + exit(1) + except subprocess.CalledProcessError: + log.error(f"java 8+ is not found") + exit(1) + log.info(f"found java: {version_line}") + + +def decompile_apk(apk: str): + if not os.path.exists(config["folders"]["decompiled"]): + log.info(f"creating `decompiled` folder: {config['folders']['decompiled']}") + os.mkdir(config["folders"]["decompiled"]) + else: + log.info(f"resetting `decompiled` folder: {config['folders']['decompiled']}") + shutil.rmtree(config["folders"]["decompiled"]) + os.mkdir(config["folders"]["decompiled"]) + + log.info(f"decompile apk: `{apk}`") + try: + result = subprocess.run( + f"java -jar {config['folders']['tools']}/apktool.jar d -f -o {config['folders']['decompiled']} {config['folders']['apks']}/{apk}", + shell=True, + check=True, + text=True, + # stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as e: + log.fatal(f"error of running a command: %s", e.stderr, exc_info=True) + exit(1)