9 Commits

17 changed files with 394 additions and 71 deletions
+65
View File
@@ -0,0 +1,65 @@
name: Build mod
on:
workflow_dispatch:
#schedule: # раз в 36 часов
# - cron: "0 0 */3 * *" # каждые 3 дня в 00:00
# - cron: "0 12 */3 * *" # каждые 3 дня в 12:00
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Download APK
run: |
curl -L -o app.apk "https://mirror-dl.anixart-app.com/anixart-beta.apk"
- name: Ensure aapt is installed
run: |
if ! command -v aapt &> /dev/null; then
echo "aapt не найден, устанавливаем..."
sudo apt-get update && sudo apt-get install -y --no-install-recommends android-sdk-build-tools
fi
- name: Export secrets
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
run: |
# Export so later steps can reference them
echo "$KEYSTORE" | base64 -d > keystore.jks
echo "$KEYSTORE_PASS" > keystore.pass
- name: Build APK
id: build
run: |
mkdir original
mv app.apk original/
python ./main.py -f
- name: Read title from report.log
id: get_title
run: |
TITLE=$(head -n 1 modified/report.log)
echo "title=${TITLE}" >> $GITHUB_OUTPUT
- name: Setup go
if: steps.build.outputs.BUILD_EXIT == '0'
uses: actions/setup-go@v4
with:
go-version: '>=1.20'
- name: Make release
if: steps.build.outputs.BUILD_EXIT == '0'
uses: https://gitea.com/actions/release-action@main
with:
title: ${{ steps.get_title.outputs.title }}
body_path: modified/report.log
draft: true
api_key: '${{secrets.RELEASE_TOKEN}}'
files: |-
modified/**-mod.apk
modified/report.log
Vendored
+2
View File
@@ -5,3 +5,5 @@ tools
__pycache__
.venv
*.jks
*.pass
+32
View File
@@ -5,6 +5,7 @@
---
### Структура проекта:
- `main.py` Главный файл
- `patches` Модули патчей
@@ -13,6 +14,37 @@
- `patches/resources` Ресурсы, используемые патчами
- `todo_drafts` Заметки для новых патчей(можно в любом формате)
### Схема
```mermaid
---
title: Процесс модифицирования приложения
---
flowchart TD
A([Оригинальный apk]) f1@==> B[поиск и выбор apk]
B f2@==> p[Декомпиляция]
subgraph p["Применение патчей по возрастанию приоритета"]
C[Патч 1] --> D
D[Патч 2] --...--> E[Патч n]
end
p f3@==> F[Сборка apk обратно]
F f4@==> G[Выравнивание zipaling]
G f5@==> H[Подпись V2+V3]
H f6@==> I([Модифицированый apk])
f1@{ animate: true }
f2@{ animate: true }
f3@{ animate: true }
f4@{ animate: true }
f5@{ animate: true }
f6@{ animate: true }
```
### Установка и использование:
1. Клонируйте репозиторий:
+12 -4
View File
@@ -3,16 +3,17 @@
"apktool_jar_url": "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.12.0.jar",
"apktool_wrapper_url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool"
},
"new_package_name": "com.wowlikon.anixart",
"new_package_name": "com.wowlikon.anixart2",
"server": "https://anixarty.wowlikon.tech/modding",
"theme": {
"colors": {
"primary": "#ccff00",
"secondary": "#ffffd700",
"background": "#FFFFFF",
"background": "#ffffff",
"text": "#000000"
},
"gradient": {
"angle": "135.0",
"from": "#ffff6060",
"to": "#ffccff00"
}
@@ -29,10 +30,17 @@
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Kentai Radiquum",
"description": "Разработчик",
"url": "https://t.me/radiquum",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
},
{
"title": "Мы в Telegram",
"description": "Подпишитесь на канал, чтобы быть в курсе последних новостей.",
"url": "https://t.me/wowlikon",
"url": "https://t.me/http_teapod",
"icon": "@drawable/ic_custom_telegram",
"icon_space_reserved": "false"
}
@@ -51,5 +59,5 @@
"android": "http://schemas.android.com/apk/res/android",
"app": "http://schemas.android.com/apk/res-auto"
},
"speeds": [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 9.0]
"speeds": [9.0]
}
+104 -26
View File
@@ -1,9 +1,12 @@
import os
import sys
import json
import yaml
import requests
import argparse
import colorama
import importlib
import traceback
import subprocess
from tqdm import tqdm
@@ -12,7 +15,7 @@ def init() -> dict:
if not os.path.exists(directory):
os.makedirs(directory)
with open("./patches/config.json", "r") as config_file:
with open("./config.json", "r") as config_file:
conf = json.load(config_file)
if not os.path.exists("./tools/apktool.jar"):
@@ -61,6 +64,11 @@ def select_apk() -> str:
print("Нет файлов .apk в текущей директории")
sys.exit(1)
if len(apks) == 1:
apk = apks[0]
print(f"Выбран файл {apk}")
return apk
while True:
print("Выберете файл для модификации")
for index, apk in enumerate(apks):
@@ -101,6 +109,53 @@ def decompile_apk(apk: str):
sys.exit(1)
def compile_apk(apk: str):
print("Компилируем apk...")
try:
subprocess.run(
"tools/apktool b decompiled -o " + os.path.join("modified", apk),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"zipalign -v 4 " + os.path.join("modified", apk) + " " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
subprocess.run(
"apksigner sign " +
"--v1-signing-enabled false " +
"--v2-signing-enabled true " +
"--v3-signing-enabled true " +
"--ks keystore.jks " +
"--ks-pass file:keystore.pass " +
"--out " + os.path.join("modified", apk.replace(".apk", "-mod.apk")) +
" " + os.path.join("modified", apk.replace(".apk", "-aligned.apk")),
shell=True,
check=True,
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
title = "anixart mod "
with open('./decompiled/apktool.yml') as f:
package = yaml.safe_load(f)
title += ' '.join([f'{k}: {v}' for k, v in package['versionInfo'].items()])
with open("./modified/report.log", "w") as log_file:
log_file.write(title+'\n')
log_file.write("\n".join([f"{patch.name}: {'applied' if patch.applied else 'failed'}" for patch in patches]))
except subprocess.CalledProcessError as e:
print("Ошибка при выполнении команды:")
print(e.stderr)
sys.exit(1)
class Patch:
def __init__(self, name, pkg):
self.name = name
@@ -117,36 +172,59 @@ class Patch:
return True
except Exception as e:
print(f"Ошибка при применении патча {self.name}: {e}")
print(e.args)
print(type(e), e.args)
traceback.print_exc()
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Автоматический патчер anixart"
)
conf = init()
apk = select_apk()
patch = decompile_apk(apk)
parser.add_argument("-v", "--verbose",
action="store_true",
help="Выводить подробные сообщения")
patches = []
for filename in os.listdir("patches/"):
if filename.endswith(".py") and filename != "__init__.py":
module_name = filename[:-3]
module = importlib.import_module(f"patches.{module_name}")
patches.append(Patch(module_name, module))
parser.add_argument("-f", "--force",
action="store_true",
help="Принудительно собрать APK")
patches.sort(key=lambda x: x.package.priority, reverse=True)
args = parser.parse_args()
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
tqdm.write(f"Применение патча: {patch.name}")
patch.apply(conf)
conf = init()
apk = select_apk()
patch = decompile_apk(apk)
statuses = {}
for patch in patches:
statuses[patch.name] = patch.applied
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + ""
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
if args.verbose: conf["verbose"] = True
if all(statuses.values()):
print("Все патчи успешно применены")
elif any(statuses.values()):
print("Некоторые патчи не были успешно применены")
else:
print("Ни один патч не был успешно применен")
patches = []
for filename in os.listdir("patches/"):
if filename.endswith(".py") and filename != "__init__.py" and not filename.startswith("todo_"):
module_name = filename[:-3]
module = importlib.import_module(f"patches.{module_name}")
patches.append(Patch(module_name, module))
patches.sort(key=lambda x: x.package.priority, reverse=True)
for patch in tqdm(patches, colour="green", desc="Применение патчей"):
tqdm.write(f"Применение патча: {patch.name}")
patch.apply(conf)
statuses = {}
for patch in patches:
statuses[patch.name] = patch.applied
marker = colorama.Fore.GREEN + "" if patch.applied else colorama.Fore.RED + ""
print(f"{marker}{colorama.Style.RESET_ALL} {patch.name}")
if all(statuses.values()):
print(f"{colorama.Fore.GREEN}Все патчи успешно применены{colorama.Style.RESET_ALL}")
compile_apk(apk)
elif any(statuses.values()):
print(f"{colorama.Fore.YELLOW}{colorama.Style.RESET_ALL} Некоторые патчи не были успешно применены")
if args.force or input("Продолжить? (y/n): ").lower() == "y":
compile_apk(apk)
else:
print(colorama.Fore.RED + "Операция отменена" + colorama.Style.RESET_ALL)
else:
print(f"{colorama.Fore.RED}Ни один патч не был успешно применен{colorama.Style.RESET_ALL}")
sys.exit(1)
+31 -6
View File
@@ -5,11 +5,36 @@ from tqdm import tqdm
import json
import requests
def apply(config: dict) -> bool:
response = requests.get(config['server'])
if response.status_code == 200:
for item in json.loads(response.text)["modding"]:
tqdm.write(item)
return True
tqdm.write(f"Failed to fetch data {response.status_code} {response.text}")
return False
assert response.status_code == 200, f"Failed to fetch data {response.status_code} {response.text}"
new_api = json.loads(response.text)
for item in new_api['modifications']:
tqdm.write(f"Изменение {item['file']}")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/network/api/'+item['file']
with open(filepath, 'r') as f:
content = f.read()
with open(filepath, 'w') as f:
if content.count(item['src']) == 0:
tqdm.write(f"Не найдено {item['src']}")
f.write(content.replace(item['src'], item['dst']))
tqdm.write(f"Изменение Github ссылки")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/utils/anixnet/GithubPagesNetFetcher.smali'
with open(filepath, 'r') as f:
content = f.read()
with open(filepath, 'w') as f:
f.write(content.replace('const-string v1, "https://anixhelper.github.io/pages/urls.json"', f'const-string v1, "{new_api["gh"]}"'))
content = ""
tqdm.write("Удаление динамического выбора сервера")
filepath = './decompiled/smali_classes2/com/swiftsoft/anixartd/DaggerApp_HiltComponents_SingletonC$SingletonCImpl$SwitchingProvider.smali'
with open(filepath, 'r') as f:
for line in f.readlines():
if "addInterceptor" in line: continue
content += line
with open(filepath, 'w') as f:
f.write(content)
return True
+5 -2
View File
@@ -5,15 +5,18 @@ from tqdm import tqdm
import os
import shutil
def apply(config: dict) -> bool:
for item in os.listdir("./decompiled/unknown/"):
item_path = os.path.join("./decompiled/unknown/", item)
if os.path.isfile(item_path):
os.remove(item_path)
tqdm.write(f'Удалён файл: {item_path}')
if config.get("verbose", False):
tqdm.write(f'Удалён файл: {item_path}')
elif os.path.isdir(item_path):
if item not in config["cleanup"]["keep_dirs"]:
shutil.rmtree(item_path)
tqdm.write(f'Удалена папка: {item_path}')
if config.get("verbose", False):
tqdm.write(f'Удалена папка: {item_path}')
return True
+57 -2
View File
@@ -1,12 +1,19 @@
"""Change application theme"""
priority = 0
from tqdm import tqdm
from lxml import etree
from utils.public import (
insert_after_public,
insert_after_color,
insert_after_id,
change_color,
)
def apply(config: dict) -> bool:
main_color = config["theme"]["colors"]["primary"]
splash_color = config["theme"]["colors"]["secondary"]
gradient_angle = config["theme"]["gradient"]["angle"]
gradient_from = config["theme"]["gradient"]["from"]
gradient_to = config["theme"]["gradient"]["to"]
@@ -31,6 +38,7 @@ def apply(config: dict) -> bool:
root = tree.getroot()
# Change attributes with namespace
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
root.set(f"{{{config['xml_ns']['android']}}}startColor", gradient_from)
root.set(f"{{{config['xml_ns']['android']}}}endColor", gradient_to)
@@ -45,7 +53,7 @@ def apply(config: dict) -> bool:
root = tree.getroot()
# Finding "path"
for el in root.findall("path", namespaces=config['xml_ns']):
for el in root.findall("path", namespaces=config["xml_ns"]):
name = el.get(f"{{{config['xml_ns']['android']}}}name")
if name == "path":
el.set(f"{{{config['xml_ns']['android']}}}fillColor", splash_color)
@@ -53,4 +61,51 @@ def apply(config: dict) -> bool:
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
for filename in ["$ic_launcher_foreground__0", "$ic_banner_foreground__0"]:
file_path = f"./decompiled/res/drawable-v24/{filename}.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
# Change attributes with namespace
root.set(f"{{{config['xml_ns']['android']}}}angle", gradient_angle)
items = root.findall("item", namespaces=config['xml_ns'])
assert len(items) == 2
items[0].set(f"{{{config['xml_ns']['android']}}}color", gradient_from)
items[1].set(f"{{{config['xml_ns']['android']}}}color", gradient_to)
# Save back
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
insert_after_public("carmine", "custom_color")
insert_after_public("carmine_alpha_10", "custom_color_alpha_10")
insert_after_color("carmine", "custom_color", main_color[0]+'ff'+main_color[1:])
insert_after_color("carmine_alpha_10", "custom_color_alpha_10", main_color[0]+'1a'+main_color[1:])
change_color("accent_alpha_10", main_color[0]+'1a'+main_color[1:])
change_color("accent_alpha_20", main_color[0]+'33'+main_color[1:])
change_color("accent_alpha_50", main_color[0]+'80'+main_color[1:])
change_color("accent_alpha_70", main_color[0]+'b3'+main_color[1:])
change_color("colorAccent", main_color[0]+'ff'+main_color[1:])
change_color("link_color", main_color[0]+'ff'+main_color[1:])
change_color("link_color_alpha_70", main_color[0]+'b3'+main_color[1:])
change_color("refresh_progress", main_color[0]+'ff'+main_color[1:])
change_color("ic_launcher_background", "#ff000000")
change_color("bottom_nav_indicator_active", "#ffffffff")
change_color("bottom_nav_indicator_icon_checked", main_color[0]+'ff'+main_color[1:])
change_color("bottom_nav_indicator_label_checked", main_color[0]+'ff'+main_color[1:])
insert_after_public("warning_error_counter_background", "ic_custom_telegram")
insert_after_public("warning_error_counter_background", "ic_custom_crown")
try:
last = "speed75"
for speed in config.get("speeds", []):
insert_after_public(last, f"speed{int(float(speed)*10)}")
insert_after_id(last, f"speed{int(float(speed)*10)}")
last = f"speed{int(float(speed)*10)}"
except Exception as e:
print(f"Error occurred while processing speeds: {e}")
return True
+5 -7
View File
@@ -1,5 +1,4 @@
"""Compress PNGs"""
priority = -1
from tqdm import tqdm
@@ -7,15 +6,15 @@ import os
import subprocess
def compress_pngs(root_dir):
def compress_pngs(root_dir: str, verbose: bool = False):
compressed_files = []
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if filename.lower().endswith(".png"):
filepath = os.path.join(dirpath, filename)
tqdm.write(f"Сжимаю: {filepath}")
if verbose: tqdm.write(f"Сжимаю: {filepath}")
try:
subprocess.run(
assert subprocess.run(
[
"pngquant",
"--force",
@@ -24,9 +23,8 @@ def compress_pngs(root_dir):
"--quality=65-90",
filepath,
],
check=True,
capture_output=True,
)
).returncode in [0, 99]
compressed_files.append(filepath)
except subprocess.CalledProcessError as e:
tqdm.write(f"Ошибка при сжатии {filepath}: {e}")
@@ -34,5 +32,5 @@ def compress_pngs(root_dir):
def apply(config: dict) -> bool:
files = compress_pngs("./decompiled")
files = compress_pngs("./decompiled", config.get("verbose", False))
return len(files) > 0 and any(files)
+1 -1
View File
@@ -1,5 +1,4 @@
"""Disable ad banners"""
priority = 0
from utils.smali_parser import (
@@ -9,6 +8,7 @@ from utils.smali_parser import (
replace_smali_method_body,
)
replace = """ .locals 0
const/4 p0, 0x1
+2 -4
View File
@@ -1,11 +1,9 @@
"""Remove beta banner"""
priority = 0
import os
from tqdm import tqdm
from lxml import etree
from typing import TypedDict
import os
from lxml import etree
def apply(config) -> bool:
-2
View File
@@ -1,7 +1,5 @@
"""Insert new files"""
priority = 0
from tqdm import tqdm
import shutil
import os
+11 -2
View File
@@ -1,9 +1,8 @@
"""Change package name of apk"""
priority = -1
from tqdm import tqdm
import os
from lxml import etree
def rename_dir(src, dst):
@@ -88,6 +87,16 @@ def apply(config: dict) -> bool:
except:
pass
file_path = "./decompiled/res/layout/fragment_sign_in.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
last_linear = root.xpath("//LinearLayout/LinearLayout[4]")[0]
last_linear.set(f"{{{config['xml_ns']['android']}}}visibility", "gone")
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return True
+1 -2
View File
@@ -1,10 +1,9 @@
"""Add new settings"""
priority = 0
from tqdm import tqdm
from lxml import etree
# Generate PreferenceCategory
def make_category(ns, name, items):
cat = etree.Element("PreferenceCategory", nsmap=ns)
cat.set(f"{{{ns['android']}}}title", name)
@@ -1,9 +1,6 @@
"""Change application icon"""
priority = 0
from tqdm import tqdm
import time
def apply(config: dict) -> bool:
time.sleep(0.2)
return False
@@ -1,7 +1,5 @@
"""Change application icon"""
priority = 0
from tqdm import tqdm
import struct
+65 -7
View File
@@ -2,15 +2,15 @@ from lxml import etree
from copy import deepcopy
def insert_after(anchor_name: str, elem_name: str):
file_path = "../decompiled/res/values/public.xml"
def insert_after_public(anchor_name: str, elem_name: str):
file_path = "./decompiled/res/values/public.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
anchor = None
types = {}
for idx, elem in enumerate(root):
for elem in root:
assert elem.tag == "public"
assert elem.keys() == ["type", "name", "id"]
attrs = dict(zip(elem.keys(), elem.values()))
@@ -25,7 +25,6 @@ def insert_after(anchor_name: str, elem_name: str):
if i not in group:
free_ids.add(i)
assert len(free_ids) > 0
new_id = None
for i in free_ids:
if i > int(anchor[1]["id"], 16):
@@ -38,12 +37,71 @@ def insert_after(anchor_name: str, elem_name: str):
if name == anchor[1]["type"]:
continue
if new_id in group:
new_id = max(group)
assert False, f"ID {new_id} already exists in group {name}"
new_elem = deepcopy(anchor[0])
new_elem.set("id", new_id)
new_elem.set("id", hex(new_id))
new_elem.set("name", elem_name)
anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
return new_id
def insert_after_id(anchor_name: str, elem_name: str):
file_path = "./decompiled/res/values/ids.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
anchor = None
for elem in root:
assert elem.tag == "item"
assert elem.keys() == ["type", "name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == anchor_name:
assert anchor == None
anchor = (elem, attrs)
new_elem = deepcopy(anchor[0])
new_elem.set("name", elem_name)
anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
def change_color(name: str, value: str):
file_path = "./decompiled/res/values/colors.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
replacements = 0
for elem in root:
assert elem.tag == "color"
assert elem.keys() == ["name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == name:
elem.set("name", name)
elem.text = value
replacements += 1
assert replacements >= 1
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")
def insert_after_color(anchor_name: str, elem_name: str, elem_value: str):
file_path = "./decompiled/res/values/colors.xml"
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser)
root = tree.getroot()
anchor = None
for elem in root:
assert elem.tag == "color"
assert elem.keys() == ["name"]
attrs = dict(zip(elem.keys(), elem.values()))
if attrs["name"] == anchor_name:
assert anchor == None
anchor = (elem, attrs)
new_elem = deepcopy(anchor[0])
new_elem.set("name", elem_name)
anchor[0].addnext(new_elem)
tree.write(file_path, pretty_print=True, xml_declaration=True, encoding="utf-8")