mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 22:34:19 +02:00
Limoka 1.4.0
This commit is contained in:
82
.github/workflows/ci.yml
vendored
82
.github/workflows/ci.yml
vendored
@@ -206,61 +206,53 @@ jobs:
|
|||||||
echo "Branch ${{ env.BRANCH_NAME }} does not exist in remote repository, skipping PR creation."
|
echo "Branch ${{ env.BRANCH_NAME }} does not exist in remote repository, skipping PR creation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
notify_diffs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||||
|
(github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true)
|
||||||
|
needs: parse
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: ${{ env.GIT_DEPTH }}
|
||||||
|
- name: Configure Git for github-actions[bot]
|
||||||
|
run: |
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip install aiohttp
|
||||||
|
- name: Send module diffs to channel
|
||||||
|
env:
|
||||||
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }}
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
python3 update_diffs.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }} --base_commit HEAD~1
|
||||||
|
|
||||||
backup:
|
backup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
|
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
|
||||||
needs: parse
|
needs: parse
|
||||||
steps:
|
steps:
|
||||||
- name: Configure Git for github-actions[bot]
|
|
||||||
run: |
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global --list
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: ${{ env.GIT_DEPTH }}
|
fetch-depth: ${{ env.GIT_DEPTH }}
|
||||||
- name: Create and send backup to Telegram
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: pip install aiohttp
|
||||||
|
- name: Run backup script
|
||||||
env:
|
env:
|
||||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
run: |
|
run: |
|
||||||
echo "Creating .zip file of the repository with maximum compression..."
|
python backup.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
git archive --format=zip --output=repository-original.zip HEAD
|
|
||||||
zip -9 repository.zip repository-original.zip
|
|
||||||
rm repository-original.zip
|
|
||||||
echo "File size of the created .zip file:"
|
|
||||||
du -sh repository.zip
|
|
||||||
echo "Splitting the .zip file into 8 parts..."
|
|
||||||
split -b 49M repository.zip repository-part-
|
|
||||||
echo "Files created after split:"
|
|
||||||
ls repository-part-*
|
|
||||||
COMMIT_MESSAGE="$(git log -1 --pretty=%B)"
|
|
||||||
COMMIT_DATE="$(date --date="$(git log -1 --pretty=%ci)" +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
COMMIT_HASH="$(git rev-parse --short=6 HEAD)"
|
|
||||||
COMMIT_URL="https://${REPO_URL}/commit/$(git rev-parse HEAD)"
|
|
||||||
MESSAGE="Commit Date: $COMMIT_DATE, Commit Message: $COMMIT_MESSAGE, Commit Hash: [\`$COMMIT_HASH\`]($COMMIT_URL)"
|
|
||||||
|
|
||||||
echo "Sending .zip file parts to Telegram..."
|
|
||||||
|
|
||||||
FIRST_PART=true
|
|
||||||
for part in $(ls repository-part-* | sort); do
|
|
||||||
if $FIRST_PART; then
|
|
||||||
TELEGRAM_API_URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument"
|
|
||||||
echo "Sending first file to Telegram: $TELEGRAM_API_URL"
|
|
||||||
curl -X POST "$TELEGRAM_API_URL" \
|
|
||||||
-F chat_id=$TELEGRAM_CHAT_ID \
|
|
||||||
-F document=@$part \
|
|
||||||
-F caption="$MESSAGE" \
|
|
||||||
-F parse_mode="Markdown"
|
|
||||||
FIRST_PART=false
|
|
||||||
else
|
|
||||||
TELEGRAM_API_URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument"
|
|
||||||
echo "Sending file to Telegram: $TELEGRAM_API_URL"
|
|
||||||
curl -X POST "$TELEGRAM_API_URL" \
|
|
||||||
-F chat_id=$TELEGRAM_CHAT_ID \
|
|
||||||
-F document=@$part
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Files sent to Telegram successfully!"
|
|
||||||
|
|||||||
74
backup.py
Normal file
74
backup.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Backup Script")
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Token of Telegram bot",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api_url",
|
||||||
|
type=str,
|
||||||
|
default="https://api.telegram.org",
|
||||||
|
help="API URL of Telegram API",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--chat_id",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Chat ID to send backup message to",
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
async def send_file(session, file_path, caption=None):
|
||||||
|
url = f"{arguments.api_url}/bot{arguments.token}/sendDocument"
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('chat_id', arguments.chat_id)
|
||||||
|
data.add_field('document', f, filename=os.path.basename(file_path))
|
||||||
|
if caption:
|
||||||
|
data.add_field('caption', caption)
|
||||||
|
data.add_field('parse_mode', 'Markdown')
|
||||||
|
async with session.post(url, data=data) as response:
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Get commit info
|
||||||
|
commit_message = subprocess.check_output(['git', 'log', '-1', '--pretty=%B']).decode().strip()
|
||||||
|
commit_date = subprocess.check_output(['git', 'log', '-1', '--pretty=%ci']).decode().strip()
|
||||||
|
commit_hash = subprocess.check_output(['git', 'rev-parse', '--short=6', 'HEAD']).decode().strip()
|
||||||
|
commit_url = f"https://github.com/MuRuLOSE/limoka/commit/{subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()}"
|
||||||
|
message = f"Commit Date: {commit_date}, Commit Message: {commit_message}, Commit Hash: [`{commit_hash}`]({commit_url})"
|
||||||
|
|
||||||
|
# Create zip
|
||||||
|
subprocess.run(['git', 'archive', '--format=zip', '--output=repository-original.zip', 'HEAD'])
|
||||||
|
subprocess.run(['zip', '-9', 'repository.zip', 'repository-original.zip'])
|
||||||
|
os.remove('repository-original.zip')
|
||||||
|
|
||||||
|
# Split zip
|
||||||
|
subprocess.run(['split', '-b', '49M', 'repository.zip', 'repository-part-'])
|
||||||
|
|
||||||
|
# Send parts
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
parts = sorted(glob.glob('repository-part-*'))
|
||||||
|
first = True
|
||||||
|
for part in parts:
|
||||||
|
caption = message if first else None
|
||||||
|
result = await send_file(session, part, caption)
|
||||||
|
print(f"Sent {part}: {result}")
|
||||||
|
first = False
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
os.remove('repository.zip')
|
||||||
|
for part in parts:
|
||||||
|
os.remove(part)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
478
parse.py
478
parse.py
@@ -1,171 +1,353 @@
|
|||||||
import os
|
|
||||||
import ast
|
import ast
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
from clone_repos import repos
|
logging.basicConfig(level=logging.WARNING, format="%(message)s")
|
||||||
from typing import Dict
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# TODO: ADD VENV IGNORE
|
def safe_unparse(node: ast.AST) -> str:
|
||||||
|
try:
|
||||||
|
return ast.unparse(node)
|
||||||
|
except AttributeError:
|
||||||
|
return getattr(node, 'id', str(type(node).__name__))
|
||||||
|
|
||||||
|
def load_blacklist(file_path):
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
repositories = data.get("repositories", [])
|
||||||
|
blacklisted_modules = {}
|
||||||
|
|
||||||
def get_module_info(module_path):
|
for i in repositories:
|
||||||
"""Парсит Python-модуль и извлекает информацию о нем."""
|
path = i.get("path", "")
|
||||||
with open(module_path, "r", encoding="utf-8") as f:
|
blacklist = i.get("blacklist", [])
|
||||||
module_content = f.read()
|
if path and blacklist:
|
||||||
|
blacklisted_modules[path] = blacklist
|
||||||
|
|
||||||
meta_info = {"pic": None, "banner": None}
|
return blacklisted_modules
|
||||||
for line in module_content.split("\n"):
|
|
||||||
if line.startswith("# meta"):
|
|
||||||
key, value = line.replace("# meta ", "").split(": ")
|
|
||||||
meta_info[key] = value
|
|
||||||
|
|
||||||
tree = ast.parse(module_content)
|
def extract_string_value(node: ast.AST) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||||
|
return node.value
|
||||||
|
if isinstance(node, ast.Str):
|
||||||
|
return node.s
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
if isinstance(node, ast.Attribute):
|
||||||
|
return f"{safe_unparse(node.value)}.{node.attr}"
|
||||||
|
return str(node)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_decorator_names(decorator_list):
|
def extract_loader_command_args(decorator: ast.Call) -> Dict[str, Any]:
|
||||||
return [ast.unparse(decorator) for decorator in decorator_list]
|
args = {"lang_docs": {}, "aliases": [], "usage": None}
|
||||||
|
try:
|
||||||
def extract_loader_command_args(decorator):
|
for kw in decorator.keywords:
|
||||||
"""Извлекает аргументы `ru_doc` и `en_doc` из `@loader.command`."""
|
arg_name = kw.arg
|
||||||
if (
|
if not arg_name:
|
||||||
isinstance(decorator, ast.Call)
|
|
||||||
and hasattr(decorator.func, "attr")
|
|
||||||
and decorator.func.attr == "command"
|
|
||||||
):
|
|
||||||
ru_doc = None
|
|
||||||
en_doc = None
|
|
||||||
for keyword in decorator.keywords:
|
|
||||||
if keyword.arg == "ru_doc":
|
|
||||||
ru_doc = ast.literal_eval(keyword.value)
|
|
||||||
elif keyword.arg == "en_doc":
|
|
||||||
en_doc = ast.literal_eval(keyword.value)
|
|
||||||
return ru_doc, en_doc
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, ast.ClassDef):
|
|
||||||
decorators = get_decorator_names(node.decorator_list)
|
|
||||||
is_tds_mod = [d for d in decorators if "loader.tds" in d]
|
|
||||||
if "Mod" not in node.name and not is_tds_mod:
|
|
||||||
continue
|
continue
|
||||||
|
if arg_name.endswith("_doc"):
|
||||||
|
lang = arg_name[:-4]
|
||||||
|
args["lang_docs"][lang] = extract_string_value(kw.value)
|
||||||
|
elif arg_name == "aliases":
|
||||||
|
try:
|
||||||
|
val = ast.literal_eval(kw.value)
|
||||||
|
if isinstance(val, (list, tuple)):
|
||||||
|
args["aliases"] = list(val)
|
||||||
|
except (ValueError, SyntaxError):
|
||||||
|
pass
|
||||||
|
elif arg_name == "usage":
|
||||||
|
args["usage"] = extract_string_value(kw.value)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return args
|
||||||
|
|
||||||
class_docstring = ast.get_docstring(node)
|
def get_module_info(module_path: str) -> Optional[Dict[str, Any]]:
|
||||||
class_info = {
|
try:
|
||||||
"name": node.name,
|
with open(module_path, "r", encoding="utf-8") as f:
|
||||||
"description": class_docstring,
|
source = f.read()
|
||||||
"meta": meta_info,
|
except Exception as e:
|
||||||
"commands": [],
|
logger.warning(f"Skipping {module_path}: read failed — {e}")
|
||||||
"new_commands": [],
|
return None
|
||||||
}
|
|
||||||
|
|
||||||
for class_body_node in node.body:
|
source = source.lstrip('\ufeff')
|
||||||
if isinstance(class_body_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
source = ''.join(c for c in source if ord(c) >= 32 or c in '\n\r\t') if source else source
|
||||||
decorators = get_decorator_names(class_body_node.decorator_list)
|
|
||||||
is_loader_command = [d for d in decorators if "command" in d]
|
|
||||||
if not is_loader_command and "cmd" not in class_body_node.name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
method_docstring = ast.get_docstring(class_body_node)
|
meta = {"pic": None, "banner": None, "developer": None}
|
||||||
command_name = class_body_node.name
|
for line in source.splitlines():
|
||||||
ru_doc, en_doc = None, None
|
line = line.strip()
|
||||||
|
if line.startswith("# meta "):
|
||||||
|
try:
|
||||||
|
key, val = line[len("# meta "):].split(":", 1)
|
||||||
|
meta[key.strip()] = val.strip()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
for decorator in class_body_node.decorator_list:
|
try:
|
||||||
ru_doc_tmp, en_doc_tmp = extract_loader_command_args(decorator)
|
tree = ast.parse(source, filename=module_path)
|
||||||
if ru_doc_tmp:
|
except SyntaxError as e:
|
||||||
ru_doc = ru_doc_tmp
|
logger.warning(f"Skipping {module_path}: syntax error — {e}")
|
||||||
if en_doc_tmp:
|
return {
|
||||||
en_doc = en_doc_tmp
|
"name": module_path.split(os.sep)[-1].replace(".py", ""),
|
||||||
|
"description": "",
|
||||||
|
"cls_doc": {},
|
||||||
|
"meta": meta,
|
||||||
|
"commands": [],
|
||||||
|
"new_commands": [],
|
||||||
|
"inline_handlers": [],
|
||||||
|
"strings": {},
|
||||||
|
"has_on_load": False,
|
||||||
|
"has_on_unload": False,
|
||||||
|
"class_cmd_names": {},
|
||||||
|
}
|
||||||
|
|
||||||
descriptions = []
|
module_data = None
|
||||||
if method_docstring:
|
|
||||||
descriptions.append(method_docstring)
|
|
||||||
if ru_doc:
|
|
||||||
descriptions.append(ru_doc)
|
|
||||||
if en_doc:
|
|
||||||
descriptions.append(en_doc)
|
|
||||||
|
|
||||||
class_info["commands"].append(
|
for node in ast.walk(tree):
|
||||||
{command_name: ' '.join(descriptions)}
|
if not isinstance(node, ast.ClassDef):
|
||||||
)
|
|
||||||
|
|
||||||
command_name = command_name.replace('cmd', '')
|
|
||||||
|
|
||||||
class_info["new_commands"].append(
|
|
||||||
{
|
|
||||||
command_name: {
|
|
||||||
"ru_doc": ru_doc,
|
|
||||||
"en_doc": en_doc,
|
|
||||||
"doc": method_docstring,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = class_info
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_developers(base_dir: str) -> Dict[str, list]:
|
|
||||||
developers = {
|
|
||||||
"repo": set(), # используем set внутри функции
|
|
||||||
"channel": set()
|
|
||||||
}
|
|
||||||
|
|
||||||
for repo_url in repos:
|
|
||||||
repo_path = repo_url.replace("https://github.com/", "")
|
|
||||||
try:
|
|
||||||
owner, repo_name = repo_path.split("/")
|
|
||||||
developers["repo"].add(owner)
|
|
||||||
except ValueError:
|
|
||||||
print(f"Incorrect URL of repository: {repo_url}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for root, _, files in os.walk(base_dir):
|
is_module_class = (
|
||||||
for file in files:
|
"Mod" in node.name or
|
||||||
if file.endswith(".py"):
|
any(isinstance(d, ast.Attribute) and safe_unparse(d).startswith("loader.tds") for d in node.decorator_list) or
|
||||||
file_path = os.path.join(root, file)
|
any(isinstance(d, ast.Name) and d.id == "loader" for d in node.decorator_list)
|
||||||
try:
|
)
|
||||||
module_info = get_module_info(file_path)
|
|
||||||
if module_info and "meta" in module_info:
|
|
||||||
developer = module_info["meta"].get('developer')
|
|
||||||
if developer: # Проверяем, что developer не None
|
|
||||||
# Разделяем строки с запятыми, &, | и пробелами
|
|
||||||
for dev in developer.replace(',', ' ').replace('&', ' ').replace('|', ' ').split():
|
|
||||||
# Добавляем только элементы, начинающиеся с @
|
|
||||||
if dev.startswith('@'):
|
|
||||||
developers["channel"].add(dev.strip())
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при парсинге файла {file_path}: {e}")
|
|
||||||
|
|
||||||
# Преобразуем set в list перед возвратом
|
if not is_module_class:
|
||||||
return {
|
continue
|
||||||
"repo": list(developers["repo"]),
|
|
||||||
"channel": list(developers["channel"])
|
info = {
|
||||||
|
"name": node.name,
|
||||||
|
"description": ast.get_docstring(node) or "",
|
||||||
|
"cls_doc": {},
|
||||||
|
"meta": meta,
|
||||||
|
"commands": [],
|
||||||
|
"new_commands": [],
|
||||||
|
"inline_handlers": [],
|
||||||
|
"strings": {},
|
||||||
|
"has_on_load": False,
|
||||||
|
"has_on_load": False,
|
||||||
|
"has_on_unload": False,
|
||||||
|
"class_cmd_names": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in node.body:
|
||||||
|
if isinstance(item, ast.Assign):
|
||||||
|
for target in item.targets:
|
||||||
|
if isinstance(target, ast.Name) and (target.id == "strings" or target.id.startswith("strings_")):
|
||||||
|
try:
|
||||||
|
lit = ast.literal_eval(item.value)
|
||||||
|
if isinstance(lit, dict):
|
||||||
|
if target.id == "strings":
|
||||||
|
info["strings"].update(lit)
|
||||||
|
if "_cls_doc" in lit:
|
||||||
|
info["cls_doc"]["default"] = lit["_cls_doc"]
|
||||||
|
else:
|
||||||
|
lang = target.id.split("_", 1)[1] if "_" in target.id else None
|
||||||
|
if lang:
|
||||||
|
for k, v in lit.items():
|
||||||
|
if isinstance(k, str) and isinstance(v, str):
|
||||||
|
if k == "_cls_doc":
|
||||||
|
info["cls_doc"][lang] = v
|
||||||
|
elif k.startswith("_cmd_doc_"):
|
||||||
|
rest = k[len("_cmd_doc_"):]
|
||||||
|
info["strings"][f"_cmd_doc_{lang}_{rest}"] = v
|
||||||
|
info["strings"][f"_cmd_doc_{rest}_{lang}"] = v
|
||||||
|
elif k.startswith("_ihandle_doc_"):
|
||||||
|
rest = k[len("_ihandle_doc_"):]
|
||||||
|
info["strings"][f"_ihandle_doc_{lang}_{rest}"] = v
|
||||||
|
info["strings"][f"_ihandle_doc_{rest}_{lang}"] = v
|
||||||
|
elif k.startswith("_cls_cmd_"):
|
||||||
|
info["class_cmd_names"][lang] = v
|
||||||
|
else:
|
||||||
|
info["strings"][f"{k}_{lang}"] = v
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if "_cls_doc" in info["strings"]:
|
||||||
|
info["cls_doc"]["default"] = info["strings"]["_cls_doc"]
|
||||||
|
|
||||||
|
for func in node.body:
|
||||||
|
if not isinstance(func, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = func.name
|
||||||
|
if name == "on_load":
|
||||||
|
info["has_on_load"] = True
|
||||||
|
continue
|
||||||
|
if name == "on_unload":
|
||||||
|
info["has_on_unload"] = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_decorated = any(
|
||||||
|
isinstance(d, ast.Call) and hasattr(d.func, 'attr') and
|
||||||
|
d.func.attr in ("command", "inline_handler", "unrestricted", "owner")
|
||||||
|
for d in func.decorator_list
|
||||||
|
)
|
||||||
|
|
||||||
|
if name.startswith("_") and not is_decorated:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cmd = {
|
||||||
|
"name": name,
|
||||||
|
"doc": ast.get_docstring(func) or "",
|
||||||
|
"lang_docs": {},
|
||||||
|
"aliases": [],
|
||||||
|
"usage": None,
|
||||||
|
"inline": False,
|
||||||
|
"is_inline_handler": False,
|
||||||
|
"decorators": [],
|
||||||
|
"cmd_names": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for dec in func.decorator_list:
|
||||||
|
if isinstance(dec, ast.Call) and hasattr(dec.func, 'attr'):
|
||||||
|
attr = dec.func.attr
|
||||||
|
if attr == "command":
|
||||||
|
cmd.update(extract_loader_command_args(dec))
|
||||||
|
elif attr == "inline_handler":
|
||||||
|
cmd["inline"] = True
|
||||||
|
cmd["is_inline_handler"] = True
|
||||||
|
elif attr in ("unrestricted", "owner", "support"):
|
||||||
|
cmd["decorators"].append(attr)
|
||||||
|
|
||||||
|
for stmt in func.body:
|
||||||
|
if isinstance(stmt, ast.Assign):
|
||||||
|
for target in stmt.targets:
|
||||||
|
if isinstance(target, ast.Attribute):
|
||||||
|
attr = target.attr
|
||||||
|
val = extract_string_value(stmt.value)
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
if attr == "_cmd":
|
||||||
|
cmd["name"] = val
|
||||||
|
elif attr == "_doc":
|
||||||
|
cmd["doc"] = val
|
||||||
|
elif attr == "_cls_doc":
|
||||||
|
info["cls_doc"]["default"] = val
|
||||||
|
elif attr.startswith("_cls_doc_"):
|
||||||
|
lang = attr[len("_cls_doc_"):]
|
||||||
|
info["cls_doc"][lang] = val
|
||||||
|
elif attr.startswith("_cmd_"):
|
||||||
|
lang = attr[len("_cmd_"):]
|
||||||
|
cmd["cmd_names"][lang] = val
|
||||||
|
|
||||||
|
is_command_name = "cmd" in name and not name.startswith("__")
|
||||||
|
if not (is_decorated or is_command_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
clean_name = cmd["name"].replace("cmd", "").replace("_", "")
|
||||||
|
|
||||||
|
descs = []
|
||||||
|
legacy_key = f"_cmd_doc_{clean_name}"
|
||||||
|
legacy_doc = info["strings"].get(legacy_key)
|
||||||
|
base_doc = legacy_doc if legacy_doc else cmd["doc"]
|
||||||
|
if base_doc:
|
||||||
|
descs.append(base_doc)
|
||||||
|
|
||||||
|
for lang, text in cmd["lang_docs"].items():
|
||||||
|
if text:
|
||||||
|
descs.append(f"({lang.upper()}) {text}")
|
||||||
|
|
||||||
|
for k, v in info["strings"].items():
|
||||||
|
if k.startswith("_cmd_doc_") and clean_name in k and v:
|
||||||
|
if k.endswith(f"_{clean_name}"):
|
||||||
|
lang_part = k[len("_cmd_doc_"):-len(f"_{clean_name}")-1]
|
||||||
|
if lang_part:
|
||||||
|
descs.append(f"({lang_part.upper()}) {v}")
|
||||||
|
elif k.startswith(f"_cmd_doc_{clean_name}_"):
|
||||||
|
lang_part = k[len(f"_cmd_doc_{clean_name}_"):]
|
||||||
|
if lang_part:
|
||||||
|
descs.append(f"({lang_part.upper()}) {v}")
|
||||||
|
|
||||||
|
full_desc = " | ".join(filter(None, descs))
|
||||||
|
info["commands"].append({clean_name: full_desc})
|
||||||
|
|
||||||
|
desc_map = {"default": legacy_doc or cmd["doc"]}
|
||||||
|
desc_map.update(cmd["lang_docs"])
|
||||||
|
|
||||||
|
for k, v in info["strings"].items():
|
||||||
|
if k.startswith("_cmd_doc_") and clean_name in k and v:
|
||||||
|
if k.endswith(f"_{clean_name}"):
|
||||||
|
lang_part = k[len("_cmd_doc_"):-len(f"_{clean_name}")-1]
|
||||||
|
if lang_part:
|
||||||
|
desc_map[lang_part] = v
|
||||||
|
elif k.startswith(f"_cmd_doc_{clean_name}_"):
|
||||||
|
lang_part = k[len(f"_cmd_doc_{clean_name}_"):]
|
||||||
|
if lang_part:
|
||||||
|
desc_map[lang_part] = v
|
||||||
|
|
||||||
|
info["new_commands"].append({
|
||||||
|
"name": clean_name,
|
||||||
|
"original_name": cmd["name"],
|
||||||
|
"description": desc_map,
|
||||||
|
"cmd_names": cmd["cmd_names"],
|
||||||
|
"aliases": cmd["aliases"],
|
||||||
|
"usage": cmd["usage"],
|
||||||
|
"inline": cmd["inline"],
|
||||||
|
"is_inline_handler": cmd["is_inline_handler"],
|
||||||
|
"decorators": cmd["decorators"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if cmd["is_inline_handler"]:
|
||||||
|
inline_desc_map = {"default": cmd["doc"]}
|
||||||
|
inline_desc_map.update(cmd["lang_docs"])
|
||||||
|
|
||||||
|
for k, v in info["strings"].items():
|
||||||
|
if k.startswith("_ihandle_doc_") and clean_name in k and v:
|
||||||
|
if k.endswith(f"_{clean_name}"):
|
||||||
|
lang_part = k[len("_ihandle_doc_"):-len(f"_{clean_name}")-1]
|
||||||
|
if lang_part:
|
||||||
|
inline_desc_map[lang_part] = v
|
||||||
|
elif k.startswith(f"_ihandle_doc_{clean_name}_"):
|
||||||
|
lang_part = k[len(f"_ihandle_doc_{clean_name}_"):]
|
||||||
|
if lang_part:
|
||||||
|
inline_desc_map[lang_part] = v
|
||||||
|
|
||||||
|
info["inline_handlers"].append({
|
||||||
|
"name": clean_name,
|
||||||
|
"description": inline_desc_map,
|
||||||
|
"decorators": cmd["decorators"],
|
||||||
|
})
|
||||||
|
|
||||||
|
module_data = info
|
||||||
|
break
|
||||||
|
|
||||||
|
return module_data
|
||||||
|
|
||||||
|
def main():
|
||||||
|
base_dir = os.getcwd()
|
||||||
|
modules = {}
|
||||||
|
blacklisted_modules = load_blacklist("repositories.json")
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(base_dir):
|
||||||
|
dirs[:] = [d for d in dirs if d not in ("venv", ".venv", "env", ".env", ".git")]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".py") and not file.startswith("_") and file not in blacklisted_modules.get(os.path.relpath(root, base_dir), []):
|
||||||
|
path = os.path.join(root, file)
|
||||||
|
try:
|
||||||
|
data = get_module_info(path)
|
||||||
|
if data:
|
||||||
|
rel = os.path.relpath(path, base_dir).replace("\\", "/")
|
||||||
|
modules[rel] = data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {path}: {e}")
|
||||||
|
|
||||||
|
output = {
|
||||||
|
"modules": modules,
|
||||||
|
"meta": {
|
||||||
|
"total_modules": len(modules),
|
||||||
|
"generated_at": __import__("datetime").datetime.now().isoformat(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with open("modules.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(output, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
modules_data = {}
|
print(f"modules.json written ({len(modules)} modules)")
|
||||||
base_dir = os.getcwd()
|
|
||||||
|
|
||||||
for root, _, files in os.walk(base_dir):
|
if __name__ == "__main__":
|
||||||
for file in files:
|
main()
|
||||||
if file.endswith(".py"):
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
try:
|
|
||||||
module_info = get_module_info(file_path)
|
|
||||||
if module_info:
|
|
||||||
relative_path = os.path.relpath(file_path, base_dir)
|
|
||||||
modules_data[relative_path] = module_info
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при парсинге файла {file_path}: {e}")
|
|
||||||
|
|
||||||
developers = parse_developers(base_dir)
|
|
||||||
|
|
||||||
with open("modules.json", "w", encoding="utf-8") as json_file:
|
|
||||||
json.dump(modules_data, json_file, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
print("Файл modules.json создан!")
|
|
||||||
|
|
||||||
with open("developers.json", "w", encoding="utf-8") as json_file:
|
|
||||||
json.dump(developers, json_file, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
print("Файл developers.json создан!")
|
|
||||||
@@ -2,191 +2,238 @@
|
|||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
"path": "DziruModules/hikkamods",
|
"path": "DziruModules/hikkamods",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "kamolgks/Hikkamods",
|
"path": "kamolgks/Hikkamods",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "thomasmod/hikkamods",
|
"path": "thomasmod/hikkamods",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "SkillsAngels/Modules",
|
"path": "SkillsAngels/Modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Sad0ff/modules-ftg",
|
"path": "Sad0ff/modules-ftg",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Yahikoro/Modules-for-FTG",
|
"path": "Yahikoro/Modules-for-FTG",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "KeyZenD/modules",
|
"path": "KeyZenD/modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "AlpacaGang/ftg-modules",
|
"path": "AlpacaGang/ftg-modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "trololo65/Modules",
|
"path": "trololo65/Modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Ijidishurka/modules",
|
"path": "Ijidishurka/modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Fl1yd/FTG-Modules",
|
"path": "Fl1yd/FTG-Modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "D4n13l3k00/FTG-Modules",
|
"path": "D4n13l3k00/FTG-Modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "iamnalinor/FTG-modules",
|
"path": "iamnalinor/FTG-modules",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "SekaiYoneya/modules",
|
"path": "SekaiYoneya/modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "GeekTG/FTG-Modules",
|
"path": "GeekTG/FTG-Modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Den4ikSuperOstryyPer4ik/Astro-modules",
|
"path": "Den4ikSuperOstryyPer4ik/Astro-modules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "vsecoder/hikka_modules",
|
"path": "vsecoder/hikka_modules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "sqlmerr/hikka_mods",
|
"path": "sqlmerr/hikka_mods",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "N3rcy/modules",
|
"path": "N3rcy/modules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "KorenbZla/HikkaModules",
|
"path": "KorenbZla/HikkaModules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "MuRuLOSE/HikkaModulesRepo",
|
"path": "MuRuLOSE/HikkaModulesRepo",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "coddrago/modules",
|
"path": "coddrago/modules",
|
||||||
"tags": ["herokutrusted", "hikkatrusted"]
|
"tags": ["herokutrusted", "hikkatrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "1jpshiro/hikka-modules",
|
"path": "1jpshiro/hikka-modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "MoriSummerz/ftg-mods",
|
"path": "MoriSummerz/ftg-mods",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "anon97945/hikka-mods",
|
"path": "anon97945/hikka-mods",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dorotorothequickend/DorotoroModules",
|
"path": "dorotorothequickend/DorotoroModules",
|
||||||
"tags": ["hikkatrusted", "nonlongermaintained"]
|
"tags": ["hikkatrusted", "nonlongermaintained"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "AmoreForever/amoremods",
|
"path": "AmoreForever/amoremods",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "idiotcoders/idiotmodules",
|
"path": "idiotcoders/idiotmodules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted", "nonactive"]
|
"tags": ["hikkatrusted", "herokutrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "CakesTwix/Hikka-Modules",
|
"path": "CakesTwix/Hikka-Modules",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "archquise/H.Modules",
|
"path": "archquise/H.Modules",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "GD-alt/mm-hikka-mods",
|
"path": "GD-alt/mm-hikka-mods",
|
||||||
"tags": ["hikkatrusted", "herokutrusted", "nonactive"]
|
"tags": ["hikkatrusted", "herokutrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "HitaloSama/FTG-modules-repo",
|
"path": "HitaloSama/FTG-modules-repo",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "SekaiYoneya/Friendly-telegram",
|
"path": "SekaiYoneya/Friendly-telegram",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "blazedzn/ftg-modules",
|
"path": "blazedzn/ftg-modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "hikariatama/ftg",
|
"path": "hikariatama/ftg",
|
||||||
"tags": ["hikkatrusted", "nonactive"]
|
"tags": ["hikkatrusted", "nonactive"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "m4xx1m/FTG",
|
"path": "m4xx1m/FTG",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "skillzmeow/skillzmods_hikka",
|
"path": "skillzmeow/skillzmods_hikka",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "fajox1/famods",
|
"path": "fajox1/famods",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "TheKsenon/MyHikkaModules",
|
"path": "TheKsenon/MyHikkaModules",
|
||||||
"tags": ["hikkatrusted", "herokutrusted"]
|
"tags": ["hikkatrusted", "herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "cryptexctl/modules-mirror",
|
"path": "cryptexctl/modules-mirror",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Ruslan-Isaev/modules",
|
"path": "Ruslan-Isaev/modules",
|
||||||
"tags": ["herokutrusted"]
|
"tags": ["herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "shadowhikka/sh.modules",
|
"path": "shadowhikka/sh.modules",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "fiksofficial/python-modules",
|
"path": "fiksofficial/python-modules",
|
||||||
"tags": ["herokutrusted"]
|
"tags": ["herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "mead0wsss/mead0wsMods",
|
"path": "mead0wsss/mead0wsMods",
|
||||||
"tags": ["herokutrusted"]
|
"tags": ["herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "SenkoGuardian/SenModules",
|
"path": "SenkoGuardian/SenModules",
|
||||||
"tags": ["herokutrusted"]
|
"tags": ["herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "ZetGoHack/nullmod",
|
"path": "ZetGoHack/nullmod",
|
||||||
"tags": ["herokutrusted"]
|
"tags": ["herokutrusted"],
|
||||||
|
"blacklist": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "yummy1gay/limoka",
|
"path": "yummy1gay/limoka",
|
||||||
"tags": []
|
"tags": [],
|
||||||
|
"blacklist": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
193
update_diffs.py
Normal file
193
update_diffs.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Update Diffs Script")
|
||||||
|
parser.add_argument(
|
||||||
|
"--token",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Token of Telegram bot",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api_url",
|
||||||
|
type=str,
|
||||||
|
default="https://api.telegram.org",
|
||||||
|
help="API URL of Telegram API",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--chat_id",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Chat ID to send updates to",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base_commit",
|
||||||
|
type=str,
|
||||||
|
default="HEAD~1",
|
||||||
|
help="Base commit to compare against",
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
async def send_message(session, text):
|
||||||
|
"""Send a text message to the channel"""
|
||||||
|
url = f"{arguments.api_url}/bot{arguments.token}/sendMessage"
|
||||||
|
data = {
|
||||||
|
'chat_id': arguments.chat_id,
|
||||||
|
'text': text,
|
||||||
|
'parse_mode': 'Markdown',
|
||||||
|
}
|
||||||
|
async with session.post(url, data=data) as response:
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def send_document(session, file_path, caption=None):
|
||||||
|
"""Send a document to the channel"""
|
||||||
|
url = f"{arguments.api_url}/bot{arguments.token}/sendDocument"
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('chat_id', arguments.chat_id)
|
||||||
|
data.add_field('document', f, filename=os.path.basename(file_path))
|
||||||
|
if caption:
|
||||||
|
data.add_field('caption', caption)
|
||||||
|
data.add_field('parse_mode', 'Markdown')
|
||||||
|
async with session.post(url, data=data) as response:
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
def get_changed_files(base_commit):
|
||||||
|
"""Get list of changed files between commits"""
|
||||||
|
try:
|
||||||
|
result = subprocess.check_output(
|
||||||
|
['git', 'diff', '--name-only', base_commit, 'HEAD'],
|
||||||
|
cwd=os.getcwd()
|
||||||
|
).decode().strip().split('\n')
|
||||||
|
return [f for f in result if f]
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_file_diff(file_path, base_commit):
|
||||||
|
"""Get diff for a specific file"""
|
||||||
|
try:
|
||||||
|
diff = subprocess.check_output(
|
||||||
|
['git', 'diff', base_commit, 'HEAD', '--', file_path],
|
||||||
|
cwd=os.getcwd()
|
||||||
|
).decode()
|
||||||
|
return diff
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def is_module_file(file_path):
|
||||||
|
"""Check if file is a Python module in a modules directory"""
|
||||||
|
# Check if it's a .py file and in a modules-like directory
|
||||||
|
return file_path.endswith('.py') and any(
|
||||||
|
part in file_path.lower() for part in [
|
||||||
|
'modules', 'mods', 'ftg', 'hikka'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_module_name(file_path):
|
||||||
|
"""Extract module name from file path"""
|
||||||
|
return Path(file_path).stem
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
changed_files = get_changed_files(arguments.base_commit)
|
||||||
|
|
||||||
|
if not changed_files:
|
||||||
|
print("No changes detected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Filter for module files only
|
||||||
|
module_files = [f for f in changed_files if is_module_file(f)]
|
||||||
|
|
||||||
|
if not module_files:
|
||||||
|
print("No module changes detected")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
for file_path in module_files:
|
||||||
|
try:
|
||||||
|
module_name = extract_module_name(file_path)
|
||||||
|
|
||||||
|
# Create message with raw GitHub URL
|
||||||
|
github_url = f"https://raw.githubusercontent.com/MuRuLOSE/limoka/refs/heads/main/{file_path}"
|
||||||
|
try:
|
||||||
|
new_hash = subprocess.check_output(
|
||||||
|
['git', 'rev-list', '-n', '1', 'HEAD', '--', file_path],
|
||||||
|
cwd=os.getcwd()
|
||||||
|
).decode().strip()
|
||||||
|
except Exception:
|
||||||
|
new_hash = 'HEAD'
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_hash = subprocess.check_output(
|
||||||
|
['git', 'rev-list', '-n', '1', arguments.base_commit, '--', file_path],
|
||||||
|
cwd=os.getcwd()
|
||||||
|
).decode().strip()
|
||||||
|
except Exception:
|
||||||
|
old_hash = arguments.base_commit
|
||||||
|
|
||||||
|
diff_url = f"https://github.com/MuRuLOSE/limoka/compare/{old_hash}...{new_hash}.diff"
|
||||||
|
message = (
|
||||||
|
f"🪼 Module {module_name} changes approved\n\n"
|
||||||
|
f"[File URL]({github_url}) | [Diff URL]({diff_url})\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get diff
|
||||||
|
diff = get_file_diff(file_path, arguments.base_commit)
|
||||||
|
|
||||||
|
if not diff:
|
||||||
|
print(f"Skipping {file_path} - no diff content")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create temporary file with diff using only module name
|
||||||
|
diff_filename = f"{module_name}.diff"
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode='w',
|
||||||
|
suffix='',
|
||||||
|
prefix='',
|
||||||
|
delete=False,
|
||||||
|
encoding='utf-8',
|
||||||
|
dir=tempfile.gettempdir()
|
||||||
|
) as tmp_file:
|
||||||
|
tmp_file.write(diff)
|
||||||
|
tmp_file_path = tmp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Rename temp file to have proper name
|
||||||
|
final_path = os.path.join(tempfile.gettempdir(), diff_filename)
|
||||||
|
os.rename(tmp_file_path, final_path)
|
||||||
|
|
||||||
|
# Send diff as document with full message as caption
|
||||||
|
doc_result = await send_document(
|
||||||
|
session,
|
||||||
|
final_path,
|
||||||
|
caption=message
|
||||||
|
)
|
||||||
|
print(f"Sent diff for {module_name}: {doc_result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending {module_name}: {e}")
|
||||||
|
finally:
|
||||||
|
# Cleanup temp files
|
||||||
|
if os.path.exists(tmp_file_path):
|
||||||
|
try:
|
||||||
|
os.remove(tmp_file_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
final_path = os.path.join(tempfile.gettempdir(), diff_filename)
|
||||||
|
if os.path.exists(final_path):
|
||||||
|
try:
|
||||||
|
os.remove(final_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {file_path}: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user