Compare commits

..

2 Commits

Author SHA1 Message Date
github-actions[bot]
086ad660ba Updated modules.json after parse 2026-01-11 01:19:41 2026-01-11 01:19:41 +00:00
github-actions[bot]
e6f45bd09e Added and updated repositories 2026-01-11 01:18:34 2026-01-11 01:18:34 +00:00
43 changed files with 45339 additions and 84204 deletions

View File

@@ -137,8 +137,9 @@ jobs:
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install --upgrade pip pip install --upgrade pip
pip install requests pip install requests scikit-learn tqdm
python3 parse.py python3 parse.py
python3 categories.py
git add modules.json git add modules.json
git commit -m "Updated modules.json after parse $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes for modules.json" git commit -m "Updated modules.json after parse $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes for modules.json"
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${REPO_URL}" git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${REPO_URL}"
@@ -205,53 +206,61 @@ 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: Set up Python 3.9 - name: Create and send backup to Telegram
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: |
python backup.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID }} echo "Creating .zip file of the repository with maximum compression..."
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!"

856
Limoka.py

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,9 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
__version__ = (5, 8, 1) #фыр __version__ = (5, 7, 0) #перепешите на меня квартиру пж
#ладно
# meta developer: @SenkoGuardianModules # meta developer: @SenkoGuardianModules
@@ -19,14 +21,10 @@ import os
import io import io
import random import random
import socket import socket
import base64
import uuid
import json
from PIL import Image
import asyncio import asyncio
import logging import logging
import tempfile import tempfile
import aiohttp import httpx
from datetime import datetime from datetime import datetime
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
import pytz import pytz
@@ -61,13 +59,11 @@ DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1"
DB_IMPERSONATION_KEY = "gemini_impersonation_chats" DB_IMPERSONATION_KEY = "gemini_impersonation_chats"
GEMINI_TIMEOUT = 840 GEMINI_TIMEOUT = 840
MAX_FFMPEG_SIZE = 90 * 1024 * 1024 MAX_FFMPEG_SIZE = 90 * 1024 * 1024
DB_KEY_MAP_KEY = "gemini_key_model_map"
CHECK_MODEL = "gemini-2.5-pro"
# requires: google-genai google-api-core pytz markdown_it_py # requires: google-genai google-api-core pytz markdown_it_py
class Gemini(loader.Module): class Gemini(loader.Module):
"""Модуль для работы с Google Gemini AI. (Поддержка видео/фото/аудио""" """Модуль для работы с Google Gemini AI (New SDK). Поддержка видео/фото/аудио и контекста пользователей."""
strings = { strings = {
"name": "Gemini", "name": "Gemini",
"cfg_api_key_doc": "API ключи Google Gemini, разделенные запятой. Будут скрыты.", "cfg_api_key_doc": "API ключи Google Gemini, разделенные запятой. Будут скрыты.",
@@ -82,8 +78,6 @@ class Gemini(loader.Module):
"cfg_impersonation_reply_chance_doc": "Вероятность ответа в режиме gauto (от 0.0 до 1.0). 0.2 = 20% шанс.", "cfg_impersonation_reply_chance_doc": "Вероятность ответа в режиме gauto (от 0.0 до 1.0). 0.2 = 20% шанс.",
"cfg_temperature_doc": "Температура генерации (креативность). От 0.0 до 2.0. По умолчанию 1.0.", "cfg_temperature_doc": "Температура генерации (креативность). От 0.0 до 2.0. По умолчанию 1.0.",
"cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.", "cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.",
"cfg_image_model_doc": "Модель Gemini для генерации изображений (например: gemini-2.5-flash-image).",
"cfg_inline_pagination_doc": "Использовать инлайн-кнопки для длинных ответов.",
"no_api_key": '❗️ <b>Api ключ(и) не настроен(ы).</b>\nПолучить Api ключ можно <a href="https://aistudio.google.com/app/apikey">здесь</a>.\n<b>Добавьте ключ(и) в конфиге модуля:</b> <code>.cfg gemini api_key</code>', "no_api_key": '❗️ <b>Api ключ(и) не настроен(ы).</b>\nПолучить Api ключ можно <a href="https://aistudio.google.com/app/apikey">здесь</a>.\n<b>Добавьте ключ(и) в конфиге модуля:</b> <code>.cfg gemini api_key</code>',
"invalid_api_key": '❗️ <b>Предоставленный API ключ недействителен.</b>\nУбедитесь, что он правильно скопирован из <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a> и что для него включен Gemini API.', "invalid_api_key": '❗️ <b>Предоставленный API ключ недействителен.</b>\nУбедитесь, что он правильно скопирован из <a href="https://aistudio.google.com/app/apikey">Google AI Studio</a> и что для него включен Gemini API.',
"all_keys_exhausted": "❗️ <b>Все доступные API ключи ({}) исчерпали свою квоту.</b>\nПопробуйте позже или добавьте новые ключи в конфиге: <code>.cfg gemini api_key</code>", "all_keys_exhausted": "❗️ <b>Все доступные API ключи ({}) исчерпали свою квоту.</b>\nПопробуйте позже или добавьте новые ключи в конфиге: <code>.cfg gemini api_key</code>",
@@ -141,7 +135,7 @@ class Gemini(loader.Module):
"gme_sent_to_saved": "💾 История экспортирована в избранное.", "gme_sent_to_saved": "💾 История экспортирована в избранное.",
"new_sdk_missing": "⚠️ <b>Для работы модуля нужна библиотека google-genai.</b>\nВыполните: <code>pip install google-genai</code>", "new_sdk_missing": "⚠️ <b>Для работы модуля нужна библиотека google-genai.</b>\nВыполните: <code>pip install google-genai</code>",
"gprompt_usage": " <b>Использование:</b>\n<code>.gprompt <текст></code> — установить промпт.\n<code>.gprompt -c</code> — очистить.\nИли ответьте на <b>.txt</b> файл.", "gprompt_usage": " <b>Использование:</b>\n<code>.gprompt <текст></code> — установить промпт.\n<code>.gprompt -c</code> — очистить.\nИли ответьте на <b>.txt</b> файл.",
"gprompt_updated": "✅ <b>Системный промпт обновлен!</b>\nДлина: {} символов.", "gprompt_updated": "✅ <b>Системный промпт обновлен!</b>\nДлина: {} симв.",
"gprompt_cleared": "🗑 <b>Системный промпт очищен.</b>", "gprompt_cleared": "🗑 <b>Системный промпт очищен.</b>",
"gprompt_current": "📝 <b>Текущий системный промпт:</b>", "gprompt_current": "📝 <b>Текущий системный промпт:</b>",
"gprompt_file_error": "❗️ <b>Ошибка чтения файла:</b> {}", "gprompt_file_error": "❗️ <b>Ошибка чтения файла:</b> {}",
@@ -149,7 +143,6 @@ class Gemini(loader.Module):
"gprompt_not_text": "❗️ Это не похоже на текстовый файл.(txt)", "gprompt_not_text": "❗️ Это не похоже на текстовый файл.(txt)",
"gmodel_no_models": "⚠️ Не удалось получить список моделей.", "gmodel_no_models": "⚠️ Не удалось получить список моделей.",
"gmodel_list_error": "❗️ Ошибка получения списка: {}", "gmodel_list_error": "❗️ Ошибка получения списка: {}",
"gimg_process": "<emoji document_id=5325547803936572038>✨</emoji> <b>Генерация...</b>\n🧠 <i>Модель: {model}</i>",
} }
TEXT_MIME_TYPES = { TEXT_MIME_TYPES = {
"text/plain", "text/markdown", "text/html", "text/css", "text/csv", "text/plain", "text/markdown", "text/html", "text/css", "text/csv",
@@ -181,8 +174,6 @@ class Gemini(loader.Module):
loader.ConfigValue("gauto_in_pm", False, "Разрешить авто-ответы в личных сообщениях (ЛС).", validator=loader.validators.Boolean()), loader.ConfigValue("gauto_in_pm", False, "Разрешить авто-ответы в личных сообщениях (ЛС).", validator=loader.validators.Boolean()),
loader.ConfigValue("google_search", False, self.strings["cfg_google_search_doc"], validator=loader.validators.Boolean()), loader.ConfigValue("google_search", False, self.strings["cfg_google_search_doc"], validator=loader.validators.Boolean()),
loader.ConfigValue("temperature", 1.0, self.strings["cfg_temperature_doc"], validator=loader.validators.Float(minimum=0.0, maximum=2.0)), loader.ConfigValue("temperature", 1.0, self.strings["cfg_temperature_doc"], validator=loader.validators.Float(minimum=0.0, maximum=2.0)),
loader.ConfigValue("inline_pagination", False, self.strings["cfg_inline_pagination_doc"], validator=loader.validators.Boolean()),
loader.ConfigValue("image_model_name", "gemini-2.5-flash-image", self.strings["cfg_image_model_doc"]),
) )
self.conversations = {} self.conversations = {}
self.gauto_conversations = {} self.gauto_conversations = {}
@@ -190,25 +181,16 @@ class Gemini(loader.Module):
self.impersonation_chats = set() self.impersonation_chats = set()
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
self.memory_disabled_chats = set() self.memory_disabled_chats = set()
self.pager_cache = {}
self.key_model_map = {}
self.prompt_presets = []
self.api_keys = []
async def client_ready(self, client, db): async def client_ready(self, client, db):
self.client = client self.client = client
self.db = db self.db = db
self.me = await client.get_me() self.me = await client.get_me()
api_key_str = self.config["api_key"]
self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else []
self.key_model_map = self.db.get(self.strings["name"], DB_KEY_MAP_KEY, {})
keys_to_remove = [k for k in self.key_model_map if k not in self.api_keys]
if keys_to_remove:
for k in keys_to_remove: del self.key_model_map[k]
self.db.set(self.strings["name"], DB_KEY_MAP_KEY, self.key_model_map)
if not GOOGLE_AVAILABLE: if not GOOGLE_AVAILABLE:
logger.error("Gemini: 'google-genai' library missing! pip install google-genai") logger.error("Gemini: 'google-genai' library missing! pip install google-genai")
return return
api_key_str = self.config["api_key"]
self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else []
self.current_api_key_index = 0 self.current_api_key_index = 0
self.conversations = self._load_history_from_db(DB_HISTORY_KEY) self.conversations = self._load_history_from_db(DB_HISTORY_KEY)
self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY) self.gauto_conversations = self._load_history_from_db(DB_GAUTO_HISTORY_KEY)
@@ -350,13 +332,16 @@ class Gemini(loader.Module):
raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode)
if regeneration and raw_hist: raw_hist = raw_hist[:-2] if regeneration and raw_hist: raw_hist = raw_hist[:-2]
for item in raw_hist: for item in raw_hist:
contents.append(types.Content(role=item['role'], parts=[types.Part(text=item['content'])])) contents.append(types.Content(
role=item['role'],
parts=[types.Part(text=item['content'])]
))
request_parts = list(current_turn_parts) request_parts = list(current_turn_parts)
if not impersonation_mode: if not impersonation_mode:
try: user_timezone = pytz.timezone(self.config["timezone"]) try: user_timezone = pytz.timezone(self.config["timezone"])
except pytz.UnknownTimeZoneError: user_timezone = pytz.utc except pytz.UnknownTimeZoneError: user_timezone = pytz.utc
now = datetime.now(user_timezone) now = datetime.now(user_timezone)
time_note = f"[System Info: Current local time is {now.strftime('%Y-%m-%d %H:%M:%S %Z')}]" time_note = f"[System note: Current time is {now.strftime('%Y-%m-%d %H:%M:%S %Z')}]"
if request_parts and getattr(request_parts[0], 'text', None): if request_parts and getattr(request_parts[0], 'text', None):
request_parts[0] = types.Part(text=f"{time_note}\n\n{request_parts[0].text}") request_parts[0] = types.Part(text=f"{time_note}\n\n{request_parts[0].text}")
else: else:
@@ -382,19 +367,22 @@ class Gemini(loader.Module):
http_opts = None http_opts = None
if proxy_config: if proxy_config:
http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config}) http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config})
client = genai.Client(api_key=api_key, http_options=http_opts) client = genai.Client(api_key=api_key, http_options=http_opts)
response = await client.aio.models.generate_content( response = await client.aio.models.generate_content(
model=self.config["model_name"], model=self.config["model_name"],
contents=contents, contents=contents,
config=gen_config config=gen_config
) )
if response.text: if response.text:
result_text = response.text result_text = response.text
was_successful = True was_successful = True
if self.config["google_search"]: search_icon = " 🌐" if self.config["google_search"]: search_icon = " 🌐"
self.current_api_key_index = current_idx self.current_api_key_index = current_idx
break break
else: raise ValueError("Empty response") else:
raise ValueError("Empty response (Safety?)")
except Exception as e: except Exception as e:
err_str = str(e).lower() err_str = str(e).lower()
if "quota" in err_str or "exhausted" in err_str or "429" in err_str: if "quota" in err_str or "exhausted" in err_str or "429" in err_str:
@@ -404,7 +392,8 @@ class Gemini(loader.Module):
last_error = e last_error = e
break break
try: try:
if not was_successful: raise last_error or RuntimeError("Unknown generation error") if not was_successful:
raise last_error or RuntimeError("Unknown generation error")
if self._is_memory_enabled(str(chat_id)): if self._is_memory_enabled(str(chat_id)):
self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode)
if impersonation_mode: return result_text if impersonation_mode: return result_text
@@ -417,25 +406,14 @@ class Gemini(loader.Module):
question_html = f"<blockquote>{utils.escape_html(request_text_for_display[:200])}</blockquote>" question_html = f"<blockquote>{utils.escape_html(request_text_for_display[:200])}</blockquote>"
text_to_send = f"{mem_ind}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n{formatted_body}" text_to_send = f"{mem_ind}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}{search_icon}\n{formatted_body}"
buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None
is_long_text = len(result_text) > 3500 if len(text_to_send) > 4096:
if is_long_text and self.config["inline_pagination"]:
chunks = self._paginate_text(result_text, 3000)
uid = uuid.uuid4().hex[:6]
header = f"{mem_ind}\n\n{self.strings['question_prefix']} <blockquote>{utils.escape_html(request_text_for_display[:100])}...</blockquote>\n\n{self.strings['response_prefix']}{search_icon}\n"
self.pager_cache[uid] = {
"chunks": chunks,
"total": len(chunks),
"header": header,
"chat_id": chat_id,
"msg_id": base_message_id
}
await self._render_page(uid, 0, call or status_msg)
elif len(text_to_send) > 4096:
file_content = (f"Вопрос: {display_prompt}\n\n════════════════════\n\nОтвет Gemini:\n{result_text}") file_content = (f"Вопрос: {display_prompt}\n\n════════════════════\n\nОтвет Gemini:\n{result_text}")
file = io.BytesIO(file_content.encode("utf-8")); file.name = "Gemini_response.txt" file = io.BytesIO(file_content.encode("utf-8"))
file.name = "Gemini_response.txt"
if call: if call:
await call.answer("Ответ длинный, отправляю файлом...", show_alert=False) await call.answer("Ответ длинный, отправляю файлом...", show_alert=False)
await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id) await self.client.send_file(call.chat_id, file, caption=self.strings["response_too_long"], reply_to=call.message_id)
await call.edit(f"{self.strings['response_too_long']}", reply_markup=None)
elif status_msg: elif status_msg:
await status_msg.delete() await status_msg.delete()
await self.client.send_file(chat_id, file, caption=self.strings["response_too_long"], reply_to=base_message_id) await self.client.send_file(chat_id, file, caption=self.strings["response_too_long"], reply_to=base_message_id)
@@ -473,78 +451,6 @@ class Gemini(loader.Module):
use_url_context=use_url_context, display_prompt=clean_args or None use_url_context=use_url_context, display_prompt=clean_args or None
) )
@loader.command()
async def gimg(self, message: Message):
"""<промпт> [реплай на фото] — Генерация/Редактирование изображений через Gemini."""
args = utils.get_args_raw(message)
reply = await message.get_reply_message()
input_bytes = None
if reply:
if reply.photo:
input_bytes = await self.client.download_media(reply, bytes)
elif reply.document and reply.document.mime_type.startswith("image/"):
input_bytes = await self.client.download_media(reply, bytes)
if not args and not input_bytes:
return await utils.answer(message, "🎨 <b>Введите промпт.</b>\nПример: <code>.gimg кот в космосе</code>")
prompt = args if args else "Describe/Modify this image"
model = self.config["image_model_name"]
m = await utils.answer(message, self.strings["gimg_process"].format(model=model))
try:
res = await self._call_google_rest(model, prompt, input_bytes)
if "error" in res:
err_msg = res["error"]["message"]
try: err_msg = json.loads(err_msg)["error"]["message"]
except: pass
raise ValueError(err_msg)
img_bytes = None
try:
parts = res["candidates"][0]["content"]["parts"]
for part in parts:
if "inlineData" in part:
img_bytes = base64.b64decode(part["inlineData"]["data"])
break
except Exception as e:
raise ValueError(f"Ошибка парсинга ответа: {e}")
if not img_bytes:
raise ValueError("Модель не вернула изображение (возможно, сработал Safety Filter).")
out = io.BytesIO(img_bytes)
out.name = f"gemini_{uuid.uuid4().hex[:6]}.jpg"
await self.client.send_file(
utils.get_chat_id(message),
out,
caption=f"🎨 <b>Gemini Image</b>\n🧠 <code>{model}</code>\n📜 <code>{utils.escape_html(prompt[:100])}</code>",
reply_to=message.id
)
await m.delete()
except Exception as e:
await utils.answer(m, f"❌ <b>Ошибка:</b>\n<code>{utils.escape_html(str(e))}</code>")
@loader.command()
async def gskey(self, message: Message):
"""[-h] — Сканировать ключи. -h: показать статус из кеша без проверки."""
args = utils.get_args_raw(message).strip()
if args in ["-h", "--having", "having"]:
premium = sum(1 for v in self.key_model_map.values() if v == 1)
free = sum(1 for v in self.key_model_map.values() if v == 0)
report = (
f"📊 <b>Статус ключей (кеш):</b>\n"
f"💎 <b>Premium/Active:</b> {premium}\n"
f"👻 <b>Free/Unknown:</b> {free}\n"
f"🔑 <b>Всего в конфиге:</b> {len(self.api_keys)}"
)
return await utils.answer(message, report)
await utils.answer(message, "<emoji document_id=5386367538735104399>⌛️</emoji> <b>Сканирую ключи...</b>\n<i>Это займет время (1.2 сек на ключ).</i>")
report, invalid_keys = await self._scan_keys(force=True)
if invalid_keys:
txt_keys = "\n".join(invalid_keys)
try:
await self.client.send_message("me", f"🚫 <b>Gemini: Найдены невалидные ключи:</b>\nУдали их из конфига:\n\n<code>{txt_keys}</code>")
report += "\n\n⚠️ <b>Список невалидных ключей отправлен в Избранное.</b>"
except:
report += "\n\n⚠️ <b>Найдены невалидные ключи.</b>"
await utils.answer(message, report)
@loader.command() @loader.command()
async def gch(self, message: Message): async def gch(self, message: Message):
"""<[id чата]> <кол-во> <вопрос> - Проанализировать историю чата.""" """<[id чата]> <кол-во> <вопрос> - Проанализировать историю чата."""
@@ -573,8 +479,6 @@ class Gemini(loader.Module):
entity = await self.client.get_entity(target_chat_id) entity = await self.client.get_entity(target_chat_id)
chat_name = utils.escape_html(get_display_name(entity)) chat_name = utils.escape_html(get_display_name(entity))
chat_log = await self._get_recent_chat_text(target_chat_id, count=count, skip_last=False) chat_log = await self._get_recent_chat_text(target_chat_id, count=count, skip_last=False)
except (ValueError, TypeError, ChatAdminRequiredError, UserNotParticipantError, ChannelPrivateError) as e:
return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e.__class__.__name__))
except Exception as e: except Exception as e:
return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e)) return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e))
full_prompt = ( full_prompt = (
@@ -767,6 +671,7 @@ class Gemini(loader.Module):
import json import json
hist = json.loads(f) hist = json.loads(f)
if not isinstance(hist, list): raise ValueError if not isinstance(hist, list): raise ValueError
cid = utils.get_chat_id(message) cid = utils.get_chat_id(message)
target = self.gauto_conversations if gauto else self.conversations target = self.gauto_conversations if gauto else self.conversations
target[str(cid)] = hist target[str(cid)] = hist
@@ -776,7 +681,7 @@ class Gemini(loader.Module):
@loader.command() @loader.command()
async def gmemfind(self, message: Message): async def gmemfind(self, message: Message):
"""[слово] — Поиск в памяти текущего чата по ключевому слову или фразе.""" """[слово] — Поиск по истории текущего чата по ключевому слову или фразе."""
q = utils.get_args_raw(message).lower() q = utils.get_args_raw(message).lower()
if not q: return await utils.answer(message, "Укажите слово для поиска.") if not q: return await utils.answer(message, "Укажите слово для поиска.")
cid = utils.get_chat_id(message) cid = utils.get_chat_id(message)
@@ -850,118 +755,6 @@ class Gemini(loader.Module):
self._save_history_sync(False) self._save_history_sync(False)
await utils.answer(message, self.strings["memory_fully_cleared"].format(n)) await utils.answer(message, self.strings["memory_fully_cleared"].format(n))
@loader.callback_handler()
async def gemini_callback_handler(self, call: InlineCall):
if not call.data.startswith("gemini:"): return
parts = call.data.split(":")
action = parts[1]
if action == "noop":
await call.answer()
return
if action == "pg":
uid = parts[2]
page = int(parts[3])
await self._render_page(uid, page, call)
return
async def _clear_callback(self, call: InlineCall, cid):
self._clear_history(cid, gauto=False)
await call.edit(self.strings["memory_cleared"], reply_markup=None)
async def _regenerate_callback(self, call: InlineCall, mid, cid):
key = f"{cid}:{mid}"
if key not in self.last_requests: return await call.answer(self.strings["no_last_request"], show_alert=True)
parts, disp = self.last_requests[key]
use_url_context = bool(re.search(r'https?://\S+', disp or ""))
await self._send_to_gemini(mid, parts, regeneration=True, call=call, chat_id_override=cid, display_prompt=disp, use_url_context=use_url_context)
async def _close_callback(self, call: InlineCall, uid: str):
"""Обрабатывает нажатие кнопки закрытия для пагинации"""
await call.answer()
if uid in self.pager_cache:
del self.pager_cache[uid]
try:
await self.client.delete_messages(call.chat_id, call.message_id)
except Exception:
try:
await call.edit("✔️ Сессия закрыта.", reply_markup=None)
except Exception:
pass
async def _render_page(self, uid, page_num, entity):
data = self.pager_cache.get(uid)
if not data:
if isinstance(entity, InlineCall):
await entity.edit("⚠️ <b>Сессия истекла (RAM cleared).</b>", reply_markup=None)
return
chunks = data["chunks"]
total = data["total"]
header = data.get("header", "")
raw_text_chunk = chunks[page_num]
safe_text = self._markdown_to_html(raw_text_chunk)
text_to_show = f"{header}<blockquote expandable>{safe_text}</blockquote>"
nav_row = []
if page_num > 0:
nav_row.append({"text": "◀️", "data": f"gemini:pg:{uid}:{page_num - 1}"})
nav_row.append({"text": f"{page_num + 1}/{total}", "data": "gemini:noop"})
if page_num < total - 1:
nav_row.append({"text": "▶️", "data": f"gemini:pg:{uid}:{page_num + 1}"})
extra_row = [{"text": "❌ Закрыть", "callback": self._close_callback, "args": (uid,)}]
if data.get("chat_id") and data.get("msg_id"):
extra_row.append({"text": "🔄", "callback": self._regenerate_callback, "args": (data['msg_id'], data['chat_id'])})
buttons = [nav_row, extra_row]
if isinstance(entity, Message):
await self.inline.form(text=text_to_show, message=entity, reply_markup=buttons)
elif isinstance(entity, InlineCall):
await entity.edit(text=text_to_show, reply_markup=buttons)
elif hasattr(entity, "edit"):
try: await entity.edit(text=text_to_show, reply_markup=buttons)
except: pass
def _paginate_text(self, text: str, limit: int) -> list:
pages = []
current_page_lines = []
current_len = 0
in_code_block = False
current_code_lang = ""
lines = text.split('\n')
for line in lines:
line_len = len(line) + 1
stripped = line.strip()
if stripped.startswith("```"):
if in_code_block:
in_code_block = False
current_code_lang = ""
else:
in_code_block = True
current_code_lang = stripped.replace("```", "").strip()
if current_len + line_len > limit:
if current_page_lines:
if in_code_block: current_page_lines.append("```")
pages.append("\n".join(current_page_lines))
current_page_lines = []
current_len = 0
if in_code_block:
header = f"```{current_code_lang}"
current_page_lines.append(header)
current_len += len(header) + 1
if line_len > limit:
chunks = [line[i:i+limit] for i in range(0, len(line), limit)]
for chunk in chunks:
if current_len + len(chunk) > limit:
pages.append("\n".join(current_page_lines))
current_page_lines = [chunk]
current_len = len(chunk)
else:
current_page_lines.append(chunk)
current_len += len(chunk)
continue
current_page_lines.append(line)
current_len += line_len
if current_page_lines:
pages.append("\n".join(current_page_lines))
return pages
@loader.watcher(only_incoming=True, ignore_edited=True) @loader.watcher(only_incoming=True, ignore_edited=True)
async def watcher(self, message: Message): async def watcher(self, message: Message):
if not hasattr(message, 'chat_id'): return if not hasattr(message, 'chat_id'): return
@@ -1013,13 +806,10 @@ class Gemini(loader.Module):
user_id = self.me.id user_id = self.me.id
user_name = get_display_name(self.me) user_name = get_display_name(self.me)
message_id = getattr(message, "id", None) message_id = getattr(message, "id", None)
if message: if message:
try: if message.sender_id:
peer_id = get_peer_id(message) user_id = message.sender_id
if peer_id:
user_id = peer_id
except (TypeError, ValueError):
if message.sender_id: user_id = message.sender_id
if message.sender: if message.sender:
user_name = get_display_name(message.sender) user_name = get_display_name(message.sender)
user_text = " ".join([p.text for p in user_parts if hasattr(p, "text") and p.text]) or "[ответ на медиа]" user_text = " ".join([p.text for p in user_parts if hasattr(p, "text") and p.text]) or "[ответ на медиа]"
@@ -1048,6 +838,7 @@ class Gemini(loader.Module):
"date": now, "date": now,
"user_id": None "user_id": None
} }
history.extend([user_entry, model_entry]) history.extend([user_entry, model_entry])
limit = self.config["max_history_length"] limit = self.config["max_history_length"]
if limit > 0 and len(history) > limit * 2: if limit > 0 and len(history) > limit * 2:
@@ -1062,6 +853,8 @@ class Gemini(loader.Module):
del d[str(cid)] del d[str(cid)]
self._save_history_sync(gauto) self._save_history_sync(gauto)
def _is_memory_enabled(self, cid): return cid not in self.memory_disabled_chats
def _markdown_to_html(self, text): def _markdown_to_html(self, text):
text = re.sub(r"^(#+)\s+(.*)", lambda m: f"<b>{m.group(2)}</b>", text, flags=re.M) text = re.sub(r"^(#+)\s+(.*)", lambda m: f"<b>{m.group(2)}</b>", text, flags=re.M)
text = re.sub(r"^([ \t]*)[-*+]\s+", r"\1• ", text, flags=re.M) text = re.sub(r"^([ \t]*)[-*+]\s+", r"\1• ", text, flags=re.M)
@@ -1118,34 +911,42 @@ class Gemini(loader.Module):
if txt.strip(): lines.append(f"{name}: {txt.strip()}") if txt.strip(): lines.append(f"{name}: {txt.strip()}")
except: pass except: pass
return "\n".join(reversed(lines)) return "\n".join(reversed(lines))
def _handle_error(self, e: Exception) -> str: def _handle_error(self, e: Exception) -> str:
logger.exception("Gemini execution error") logger.exception("Gemini execution error")
if isinstance(e, asyncio.TimeoutError): if isinstance(e, asyncio.TimeoutError):
return self.strings["api_timeout"] return self.strings["api_timeout"]
if google_exceptions and isinstance(e, google_exceptions.GoogleAPIError):
msg = str(e)
if "quota" in msg.lower() or "exceeded" in msg.lower():
model = self.config.get("model_name", "unknown")
return (
f"❗️ <b>Превышен лимит Google Gemini API для модели <code>{utils.escape_html(model)}</code>.</b>\n"
f"<b>Детали ошибки:</b>\n<code>{utils.escape_html(msg)}</code>"
)
if "User location is not supported" in msg or "location is not supported" in msg:
return (
'❗️ <b>В данном регионе Gemini API не доступен.</b>\n'
'Используйте VPN или прокси.'
)
if "API key not valid" in msg:
return self.strings["invalid_api_key"]
if "blocked" in msg.lower():
return self.strings["blocked_error"].format(utils.escape_html(msg))
return self.strings["api_error"].format(utils.escape_html(msg))
if isinstance(e, (OSError, socket.timeout)):
return "❗️ <b>Сетевая ошибка:</b>\n<code>{}</code>".format(utils.escape_html(str(e)))
msg = str(e) msg = str(e)
if "quota" in msg.lower() or "429" in msg: return self.strings["all_keys_exhausted"].format(len(self.api_keys)) if "quota" in msg.lower() or "exhausted" in msg.lower() or "429" in msg:
return self.strings["generic_error"].format(utils.escape_html(msg)) model = self.config.get("model_name", "unknown")
return (
f"❗️ <b>Превышен лимит Google Gemini API для модели <code>{utils.escape_html(model)}</code>.</b>"
"\n\nЧаще всего это происходит на бесплатном тарифе. Вы можете:\n"
"• Подождать, пока лимит сбросится (обычно раз в сутки).\n"
"• Проверить свой тарифный план в <a href='https://aistudio.google.com/app/billing'>Google AI Studio</a>.\n"
"• Узнать больше о лимитах <a href='https://ai.google.dev/gemini-api/docs/rate-limits'>здесь</a>.\n\n"
f"<b>Детали ошибки:</b>\n<code>{utils.escape_html(msg)}</code>"
)
if "location" in msg.lower() or "not supported" in msg.lower():
return (
'❗️ <b>В данном регионе Gemini API не доступен.</b>\n'
'Скачайте VPN (для пк/тел) или поставьте прокси (платный/бесплатный).\n'
'Или воспользуйтесь инструкцией <a href="https://t.me/SenkoGuardianModules/23">вот тут</a>\n'
'А для тех у кого UserLand инструкция <a href="https://t.me/SenkoGuardianModules/35">тут</a>'
)
if "key" in msg.lower() and "valid" in msg.lower():
return self.strings["invalid_api_key"]
if "blocked" in msg.lower():
return self.strings["blocked_error"].format(utils.escape_html(msg))
if "500" in msg:
return (
"❗️ <b>Ошибка 500 от Google API.</b>\n"
"Это значит, что формат медиа (файл или еще что то) который ты отправил, не поддерживается.\n"
"Такое случается, по такой причине:\n "
"• Если формат файла в принципе не поддерживается Gemini/Гуглом.\n "
"• Временный сбой на серверах Google. Попробуйте повторить запрос позже."
)
return self.strings["api_error"].format(utils.escape_html(msg))
def _markdown_to_html(self, text: str) -> str: def _markdown_to_html(self, text: str) -> str:
def heading_replacer(match): def heading_replacer(match):
@@ -1200,7 +1001,24 @@ class Gemini(loader.Module):
async def _clear_callback(self, call: InlineCall, chat_id: int): async def _clear_callback(self, call: InlineCall, chat_id: int):
self._clear_history(chat_id, gauto=False) self._clear_history(chat_id, gauto=False)
await call.edit(self.strings["memory_cleared"], reply_markup=None) await call.edit(self.strings["memory_cleared"], reply_markup=None)
async def _regenerate_callback(self, call: InlineCall, original_message_id: int, chat_id: int):
key = f"{chat_id}:{original_message_id}"
last_request_tuple = self.last_requests.get(key)
if not last_request_tuple:
return await call.answer(self.strings["no_last_request"], show_alert=True)
last_parts, display_prompt = last_request_tuple
use_url_context = bool(re.search(r'https?://\S+', display_prompt or ""))
await self._send_to_gemini(
message=original_message_id,
parts=last_parts,
regeneration=True,
call=call,
chat_id_override=chat_id,
use_url_context=use_url_context,
display_prompt=display_prompt
)
async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str: async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str:
history_limit = count or self.config["impersonation_history_limit"] history_limit = count or self.config["impersonation_history_limit"]
@@ -1231,111 +1049,6 @@ class Gemini(loader.Module):
logger.warning(f"Не удалось получить историю для авто-ответа: {e}") logger.warning(f"Не удалось получить историю для авто-ответа: {e}")
return "\n".join(reversed(chat_history_lines)) return "\n".join(reversed(chat_history_lines))
async def _scan_keys(self, force=False):
"""
Сканирует ключи на валидность.
"""
if not GOOGLE_AVAILABLE: return "Library missing", []
current_map_keys = list(self.key_model_map.keys())
for k in current_map_keys:
if k not in self.api_keys: del self.key_model_map[k]
if not force and all(k in self.key_model_map for k in self.api_keys):
return "Loaded from cache", []
if force: self.key_model_map = {}
proxy_config = self._get_proxy_config()
http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config, "timeout": 10.0}) if proxy_config else None
active_keys = []
invalid_keys = []
minimal_config = types.GenerateContentConfig(
response_mime_type="text/plain",
max_output_tokens=1,
candidate_count=1,
safety_settings=[types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE")]
)
for i, key in enumerate(self.api_keys):
if i > 0: await asyncio.sleep(1.2)
try:
client = genai.Client(api_key=key, http_options=http_opts)
response = await client.aio.models.generate_content(
model=CHECK_MODEL, contents="test", config=minimal_config
)
active_keys.append(key)
self.key_model_map[key] = 1
except Exception as e:
err = str(e).lower()
if "invalid_argument" in err or "api_key_invalid" in err or "400" in err or "blocked" in err:
invalid_keys.append(key)
else:
self.key_model_map[key] = 0
self.db.set(self.strings["name"], DB_KEY_MAP_KEY, self.key_model_map)
short_report = (
f"✅ <b>Скан завершен.</b>\n"
f"💎 <b>Active:</b> {len(active_keys)}\n"
f"🗑 <b>Invalid:</b> {len(invalid_keys)}\n"
f"👻 <b>RateLimited/Other:</b> {len(self.api_keys) - len(active_keys) - len(invalid_keys)}"
)
return short_report, invalid_keys
def _get_sorted_keys(self):
valid_keys = []
for key in self.api_keys:
if key not in self.key_model_map:
if not self.key_model_map: valid_keys.append((key, 0, random.random()))
continue
tier = self.key_model_map[key]
valid_keys.append((key, tier, random.random()))
valid_keys.sort(key=lambda x: (x[1], x[2]))
return [item[0] for item in valid_keys]
async def _call_google_rest(self, model_name: str, prompt: str, input_image_bytes=None):
keys = self._get_sorted_keys()
if not keys: return {"error": {"message": "Нет доступных API ключей"}}
parts = [{"text": prompt}]
if input_image_bytes:
resized = await utils.run_sync(self._resize_image_ig, input_image_bytes)
b64_img = base64.b64encode(resized).decode('utf-8')
parts.insert(0, {"inlineData": {"mimeType": "image/jpeg", "data": b64_img}})
payload = {
"contents": [{"parts": parts}],
"safetySettings": [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}
],
"generationConfig": {"candidateCount": 1, "temperature": 1.0}
}
proxy = self.config['proxy'] if self.config['proxy'] else None
last_error = None
async with aiohttp.ClientSession() as session:
for i, api_key in enumerate(keys):
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={api_key}"
try:
if i > 0: await asyncio.sleep(1)
async with session.post(url, json=payload, proxy=proxy, timeout=60) as resp:
if resp.status == 200:
return await resp.json()
elif resp.status in [429, 503, 403]:
last_error = f"HTTP {resp.status}"
continue
else:
text = await resp.text()
return {"error": {"message": f"HTTP {resp.status}: {text}"}}
except Exception as e:
last_error = str(e)
continue
return {"error": {"message": f"All keys exhausted. Last error: {last_error}"}}
def _resize_image_ig(self, img_bytes):
try:
img = Image.open(io.BytesIO(img_bytes))
img.thumbnail((1024, 1024))
out = io.BytesIO()
if img.mode in ("RGBA", "P"): img = img.convert("RGB")
img.save(out, format='JPEG', quality=85)
return out.getvalue()
except: return img_bytes
def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats
def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id)) def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id))
def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id)) def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id))

View File

@@ -1,74 +0,0 @@
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())

126
categories.py Normal file
View File

@@ -0,0 +1,126 @@
import json
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier
from sklearn.preprocessing import MultiLabelBinarizer
from tqdm import tqdm
import numpy as np
# Тренировочные данные (48 модулей)
training_data = {
"MuRuLOSE/HikkaModulesRepo/filters.py": ["Tools", "Chat"],
"MuRuLOSE/HikkaModulesRepo/autogiveawayjoin.py": ["Automation", "Social"],
"MuRuLOSE/HikkaModulesRepo/HTTPCat.py": ["Fun"],
"MuRuLOSE/HikkaModulesRepo/CustomPing.py": ["Tools", "Networking"],
"MuRuLOSE/HikkaModulesRepo/FuckTagOne.py": ["Moderation"],
"MuRuLOSE/HikkaModulesRepo/InlineButtons.py": ["Tools", "Chat"],
"MuRuLOSE/HikkaModulesRepo/YoutubeDL.py": ["Media"],
"MuRuLOSE/HikkaModulesRepo/youtubesearcher.py": ["Media", "Tools"],
"MuRuLOSE/HikkaModulesRepo/INumber.py": ["Fun", "Info"],
"MuRuLOSE/HikkaModulesRepo/RandomDog.py": ["Fun"],
"MuRuLOSE/HikkaModulesRepo/RemoveLinks.py": ["Moderation", "Chat"],
"MuRuLOSE/HikkaModulesRepo/SteamClient.py": ["Games", "Tools"],
"MuRuLOSE/HikkaModulesRepo/PinMoreChats.py": ["Chat", "Productivity"],
"MuRuLOSE/HikkaModulesRepo/MindGameCheat.py": ["Games", "Tools"],
"MuRuLOSE/HikkaModulesRepo/NasaImages.py": ["Media", "Info"],
"MuRuLOSE/HikkaModulesRepo/autoreader.py": ["Automation", "Chat"],
"MuRuLOSE/HikkaModulesRepo/K.py": ["Fun"],
"MuRuLOSE/HikkaModulesRepo/Genshin.py": ["Games"],
"MuRuLOSE/HikkaModulesRepo/compliments.py": ["Social", "Fun"],
"MuRuLOSE/HikkaModulesRepo/AutoLeave.py": ["Automation", "Chat"],
"MuRuLOSE/HikkaModulesRepo/ToTHosting.py": ["Tools", "Admin"],
"MuRuLOSE/HikkaModulesRepo/PasswordUtils.py": ["Security", "Tools"],
"MuRuLOSE/HikkaModulesRepo/FuckJoins.py": ["Security", "Chat"],
"MuRuLOSE/HikkaModulesRepo/SpyEVO.py": ["Tools", "Info"],
"MuRuLOSE/HikkaModulesRepo/FindID.py": ["Tools", "Admin"],
"MuRuLOSE/HikkaModulesRepo/ChannelCheck.py": ["Tools", "Social"],
"MuRuLOSE/HikkaModulesRepo/controlspam.py": ["Chat", "Tools"],
"MuRuLOSE/HikkaModulesRepo/VKMusic.py": ["Media"],
"MuRuLOSE/HikkaModulesRepo/morse.py": ["Tools", "Fun"],
"MuRuLOSE/HikkaModulesRepo/YamiManager.py": ["Chat", "Tools"],
"MuRuLOSE/HikkaModulesRepo/SearchersGenQuery.py": ["Tools", "Info"],
"MuRuLOSE/HikkaModulesRepo/Limoka.py": ["Utilities", "Tools"],
"MuRuLOSE/HikkaModulesRepo/CheckTime.py": ["Productivity", "Info"],
"MuRuLOSE/HikkaModulesRepo/ReplaceWords.py": ["Chat", "Customization"],
"MuRuLOSE/HikkaModulesRepo/TempJoinChannel.py": ["Chat", "Automation"],
"MuRuLOSE/HikkaModulesRepo/timer.py": ["Productivity", "Tools"],
"den4ikSuperOstryyPer4ik/astro-modules/astroafk.py": ["Automation", "Customization"],
"den4ikSuperOstryyPer4ik/astro-modules/akinator.py": ["Games"],
"den4ikSuperOstryyPer4ik/astro-modules/Emotions.py": ["Social", "Fun"],
"den4ikSuperOstryyPer4ik/astro-modules/RandomStatuses.py": ["Social", "Fun"],
"den4ikSuperOstryyPer4ik/astro-modules/RandomTrack.py": ["Media", "Fun"],
"den4ikSuperOstryyPer4ik/astro-modules/minesweeper.py": ["Games"],
"den4ikSuperOstryyPer4ik/astro-modules/inline_bot_manager.py": ["Tools", "Automation"],
"MuRuLOSE/HikkaModulesRepo/ReplaceWords.py": ["Customization", "Chat"],
"MuRuLOSE/HikkaModulesRepo/CheckTime.py": ["Productivity"],
"MuRuLOSE/HikkaModulesRepo/SearchersGenQuery.py": ["Utilities", "Info"]
}
all_categories = [
"Utilities", "Fun", "Admin", "Media", "Games", "Tools", "Security", "Social",
"Automation", "Info", "Chat", "Moderation", "Productivity", "Customization",
"Networking", "Education", "Finance", "Health", "Creative", "Other"
]
def get_module_text(module_path, module_data):
name = module_data.get("name", "").lower()
description = (module_data.get("description", "") or module_data.get("meta", {}).get("desc", "")).lower()
if not description or description == "desc":
description = ""
commands_text = " ".join([f"{cmd} {desc}".lower() for func in module_data.get("commands", []) for cmd, desc in func.items()])
new_commands_text = " ".join([f"{cmd} {data.get('doc', '')} {data.get('ru_doc', '') or ''}".lower()
for func in module_data.get("new_commands", []) for cmd, data in func.items()])
file_path = module_path.lower()
file_name = file_path.split("/")[-1]
return f"{file_name} {name} {description} {file_path} {commands_text} {new_commands_text}".strip()
with open("modules.json", "r", encoding="utf-8") as f:
modules = json.load(f)
# Подготовка тренировочных данных
train_texts = [get_module_text(path, modules[path]) for path in training_data.keys() if path in modules]
train_labels = [training_data[path] for path in training_data.keys() if path in modules]
# Векторизация текста
vectorizer = TfidfVectorizer(max_features=2000)
X_train = vectorizer.fit_transform(train_texts)
# Преобразование меток
mlb = MultiLabelBinarizer(classes=all_categories)
y_train = mlb.fit_transform(train_labels)
# Обучение модели с балансировкой классов
base_clf = LogisticRegression(class_weight="balanced", max_iter=1000)
clf = OneVsRestClassifier(base_clf)
clf.fit(X_train, y_train)
# Обработка всех модулей
print("Classifying all modules...")
texts = [get_module_text(path, data) for path, data in modules.items()]
X_all = vectorizer.transform(texts)
# Предсказание вероятностей
probs = clf.predict_proba(X_all)
# Присваивание категорий
threshold = 0.2 # Снижаем порог для большего разнообразия
for module_path, prob_vector in tqdm(zip(modules.keys(), probs), total=len(modules), desc="Assigning categories"):
module_data = modules[module_path]
sorted_indices = np.argsort(prob_vector)[::-1]
sorted_probs = prob_vector[sorted_indices]
sorted_labels = mlb.classes_[sorted_indices]
selected_categories = [label for label, prob in zip(sorted_labels, sorted_probs) if prob >= threshold][:2]
if not selected_categories:
selected_categories = ["Other"]
module_data["category"] = selected_categories
print(f"Module: {module_path} -> Categories: {selected_categories} (top probs: {[f'{p:.2f}' for p in sorted_probs[:3]]})")
# Сохранение результата
with open("modules.json", "w", encoding="utf-8") as f:
json.dump(modules, f, ensure_ascii=False, indent=2)
print("Done! Check modules_categorized.json.")

View File

@@ -3,18 +3,58 @@ import shutil
import subprocess import subprocess
import re import re
import requests import requests
import json
def parse_repos(file_path: str) -> list: repos = [
"""Parse repository URLs from a given json file.""" "https://github.com/DziruModules/hikkamods",
repos = [] "https://github.com/kamolgks/Hikkamods",
with open(file_path, "r", encoding="utf-8") as f: "https://github.com/thomasmod/hikkamods",
data = json.load(f) "https://github.com/SkillsAngels/Modules",
for repo in data.get("repositories", []): "https://github.com/Sad0ff/modules-ftg",
repos.append(repo["url"]) "https://github.com/Yahikoro/Modules-for-FTG",
return repos "https://github.com/KeyZenD/modules",
"https://github.com/AlpacaGang/ftg-modules",
repos = parse_repos("repositories.json") "https://github.com/trololo65/Modules",
"https://github.com/Ijidishurka/modules",
"https://github.com/Fl1yd/FTG-Modules",
"https://github.com/D4n13l3k00/FTG-Modules",
"https://github.com/iamnalinor/FTG-modules",
"https://github.com/SekaiYoneya/modules",
"https://github.com/GeekTG/FTG-Modules",
"https://github.com/Den4ikSuperOstryyPer4ik/Astro-modules",
"https://github.com/vsecoder/hikka_modules",
"https://github.com/sqlmerr/hikka_mods",
"https://github.com/N3rcy/modules",
"https://github.com/KorenbZla/HikkaModules",
"https://github.com/MuRuLOSE/HikkaModulesRepo",
"https://github.com/coddrago/modules",
"https://github.com/1jpshiro/hikka-modules",
"https://github.com/MoriSummerz/ftg-mods",
"https://github.com/anon97945/hikka-mods",
"https://github.com/dorotorothequickend/DorotoroModules",
"https://github.com/AmoreForever/amoremods",
"https://github.com/idiotcoders/idiotmodules",
"https://github.com/CakesTwix/Hikka-Modules",
"https://github.com/archquise/H.Modules",
"https://github.com/GD-alt/mm-hikka-mods",
"https://github.com/HitaloSama/FTG-modules-repo",
"https://github.com/SekaiYoneya/Friendly-telegram",
"https://github.com/blazedzn/ftg-modules",
"https://github.com/hikariatama/ftg",
"https://github.com/m4xx1m/FTG",
"https://github.com/skillzmeow/skillzmods_hikka",
"https://github.com/fajox1/famods",
"https://github.com/unneyon/hikka-mods",
"https://github.com/TheKsenon/MyHikkaModules",
"https://github.com/cryptexctl/modules-mirror",
"https://github.com/Ruslan-Isaev/modules",
"https://github.com/shadowhikka/sh.modules",
"https://github.com/fiksofficial/python-modules",
"https://github.com/mead0wsss/mead0wsMods",
"https://github.com/SenkoGuardian/SenModules",
"https://github.com/ZetGoHack/nullmod",
"https://github.com/yummy1gay/limoka"
]
def configure_git(): def configure_git():
"""Configure Git to ignore file mode changes.""" """Configure Git to ignore file mode changes."""

File diff suppressed because it is too large Load Diff

View File

@@ -22,5 +22,4 @@ dbmod
chatmodule chatmodule
stats stats
tagwatcher tagwatcher
hardspam hardspam
YaMusic

View File

@@ -1,5 +1,4 @@
# meta developer: @codrago_m # meta developer: @codrago_m
# scope: heroku_min 2.0.0
import logging import logging
from .. import utils, loader, main from .. import utils, loader, main
@@ -114,8 +113,15 @@ class TagWatcher(loader.Module):
validator=loader.validators.Boolean(), validator=loader.validators.Boolean(),
), ),
) )
async def client_ready(self): async def client_ready(self):
self.asset_channel = self._db.get("heroku.forums", "channel_id", 0) await self.request_join("@xdesai_modules", self.strings["request_join_reason"])
self.xdlib = await self.import_lib(
"https://raw.githubusercontent.com/xdesai96/modules/refs/heads/main/libs/xdlib.py",
suspend_on_error=True,
)
self.asset_channel = self._db.get("legacy.forums", "channel_id", 0)
self._notif_topic = await utils.asset_forum_topic( self._notif_topic = await utils.asset_forum_topic(
self._client, self._client,
self._db, self._db,
@@ -124,10 +130,6 @@ class TagWatcher(loader.Module):
description="Here will be notifications about mentions in chats.", description="Here will be notifications about mentions in chats.",
icon_emoji_id=5409025823388741707, icon_emoji_id=5409025823388741707,
) )
self.xdlib = await self.import_lib(
"https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/libs/xdlib.py",
suspend_on_error=True,
)
async def render_text(self, m): async def render_text(self, m):
if self.config["custom_notif_text"]: if self.config["custom_notif_text"]:
@@ -172,7 +174,7 @@ class TagWatcher(loader.Module):
user_id=id, user_id=id,
msg_content=msg_content, msg_content=msg_content,
reply_content=reply_content, reply_content=reply_content,
link=await m.link(), link=await m.link,
) )
@loader.command( @loader.command(
@@ -240,4 +242,4 @@ class TagWatcher(loader.Module):
message_thread_id=self._notif_topic.id, message_thread_id=self._notif_topic.id,
) )
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)

BIN
fajox1/famods/.DS_Store vendored Normal file

Binary file not shown.

BIN
fajox1/famods/assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
fajox1/famods/assets/banners/.DS_Store vendored Normal file

Binary file not shown.

BIN
fajox1/famods/assets/birds/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, ai, assistant
from .. import loader, utils from .. import loader, utils
import aiohttp import aiohttp

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, ai, username
# requires: aiohttp # requires: aiohttp
import asyncio import asyncio

View File

@@ -2,7 +2,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, user, profile
from hikkatl.types import Message from hikkatl.types import Message
from telethon.tl.functions.account import UpdateProfileRequest from telethon.tl.functions.account import UpdateProfileRequest

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, calculator, calc
from .. import loader, utils from .. import loader, utils
import math import math

View File

@@ -1,8 +1,7 @@
# На модуль распространяется лицензия "GNU General Public License v3.0 # На модуль распространяется лицензия "GNU General Public License v3.0"
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @PyModule # meta developer: @PyModule
# meta fhsdesc: tool, tools, channel, admintools, admin, admintool
from telethon.tl.types import Message from telethon.tl.types import Message
from .. import loader from .. import loader

View File

@@ -1,4 +1,3 @@
# meta fhsdesc: tool, tools, server, admin
from .. import loader, utils from .. import loader, utils
import aiohttp import aiohttp
import asyncio import asyncio

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, fun, packs
# requires: opencv-python pillow # requires: opencv-python pillow
import os import os

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: fun, cute, message, love
import random import random
import re import re

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, phone, info
# requires: aiohttp cachetools # requires: aiohttp cachetools
import asyncio import asyncio
@@ -106,7 +105,7 @@ class DeviceInfo(loader.Module):
self.config = loader.ModuleConfig( self.config = loader.ModuleConfig(
loader.ConfigValue( loader.ConfigValue(
"api_base_url", "api_base_url",
"https://gmsarena.vercel.app/", "https://mobilespecs.fiksofficial.fun",
lambda: "API Url", lambda: "API Url",
validator=loader.validators.String() validator=loader.validators.String()
), ),

View File

@@ -3,7 +3,6 @@
# scope: hikka_only # scope: hikka_only
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, scanner, domain
# requires: python-whois dnspython requests # requires: python-whois dnspython requests
import socket import socket

View File

@@ -2,7 +2,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @PyModule # meta developer: @PyModule
# meta fhsdesc: tool, tools, user, id
from .. import loader, utils from .. import loader, utils
@loader.tds @loader.tds

View File

@@ -11,459 +11,183 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, github, info, inline
from .. import loader, utils from .. import loader, utils
from ..inline import InlineCall
import logging import logging
import asyncio
import urllib.request
import json import json
import re
import urllib.request
from datetime import datetime, timedelta from datetime import datetime, timedelta
@loader.tds @loader.tds
class GitHubInfoMod(loader.Module): class GitHubInfoMod(loader.Module):
"""GitHub user information""" """GitHub user info, recent activity and contribution graph"""
strings = { strings = {
"name": "GitHubInfo", "name": "GitHubInfo",
"no_username": "❗ Provide a GitHub username.", "no_username": "❗ Provide a GitHub username.",
"user_not_found": "🚫 User not found: <b>{}</b>", "user_not_found": "🚫 User not found: <b>{}</b>",
"api_error": "⚠ GitHub API error: <b>{msg}</b>", "profile": "Profile",
"no_activity": "🕸 No recent activity from <b>{}</b>", "no_activity": "🕸 No recent activity from <b>{}</b>",
"no_contrib": "📭 No contribution data.", "no_contrib": "📭 No contribution data for <b>{}</b>",
"no_repos": "📭 No public repositories.", "info_text": (
"no_orgs": "📭 Not a member of any organizations.", "👤 <b>{name}</b> | <a href=\"{url}\">{profile}</a>\n"
"no_title": "No title", "🏢 {company} | 📍 {location}\n"
"no_desc": "No description", "📝 {bio}\n\n"
"not_specified": "Not specified", "📦 Repos: <b>{repos}</b> | "
"more_commits": " ... and {} more\n", "👥 Followers: <b>{followers}</b> | "
"hireable_yes": "Yes",
"hireable_no": "No",
"menu_text": "Choose a section:",
"btn_activity": "🔥 Activity",
"btn_contrib": "📊 Contributions",
"btn_repos": "📦 Repositories",
"btn_orgs": "🏛 Organizations",
"btn_back": "← Back to profile",
"profile_header": "<b>Profile</b> <a href=\"{url}\">{username}</a>\n\n",
"profile_text": (
"👤 Name: <b>{name}</b>\n"
"🏷 Login: <code>{login}</code>\n"
"📝 Bio: {bio}\n"
"🏢 Company: {company}\n"
"📍 Location: {location}\n"
"📧 Email: {email}\n"
"🔗 Website: {blog}\n"
"🐦 Twitter: {twitter}\n"
"💼 Hireable: {hireable}\n"
"📊 Type: {type}\n"
"📦 Public repos: <b>{repos}</b>\n"
"⭐ Public gists: <b>{gists}</b>\n"
"👥 Followers: <b>{followers}</b>\n"
"👣 Following: <b>{following}</b>\n" "👣 Following: <b>{following}</b>\n"
"🕐 Created: <code>{created}</code>\n" "🕒 Created: <code>{created}</code>"
"🕐 Updated: <code>{updated}</code>"
), ),
"activity_header": "<b>Recent activity:</b>\n",
"activity_header": "<b>Recent activity</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n", "activity_commit": "🔨 {count} commit(s) → <code>{branch}</code> in {repo}",
"activity_create": "✨ Created {ref_type} in {repo}",
"push_header": "🔨 Pushed to <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_pr": "🔄 {action} PR: {title}",
"push_no_commits": "🔨 Pushed (no details) to <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_issue": "{action} issue: {title}",
"commit_line": "• <a href=\"{url}\"><code>{sha}</code></a>: {message}\n", "activity_star": "⭐ Starred {repo}",
"activity_fork": "⑂ Forked to {fork}",
"create_branch": "✨ Created branch <code>{ref}</code> in <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_other": "{event} in {repo}",
"create_tag": "✨ Created tag <code>{ref}</code> in <a href=\"https://github.com/{repo}/releases/tag/{ref}\">{repo}</a>\n", "contrib_header": "<b>Contribution graph</b> for <a href=\"https://github.com/{username}\">{username}</a>:\n",
"create_repo": "✨ Created repository <a href=\"https://github.com/{repo}\">{repo}</a>\n", "contrib_footer": "⬛ = 0, 🟩 = 1+ contributions",
"pr_opened": "🔄 Opened PR <a href=\"{url}\">#{} {title}</a>\n",
"pr_closed": "🔄 Closed PR <a href=\"{url}\">#{} {title}</a>\n",
"pr_merged": "🔄 Merged PR <a href=\"{url}\">#{} {title}</a>\n",
"issue_opened": "❗ Opened issue <a href=\"{url}\">#{} {title}</a>\n",
"issue_closed": "❗ Closed issue <a href=\"{url}\">#{} {title}</a>\n",
"star": "⭐ Starred <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"fork": "⑂ Forked <a href=\"https://github.com/{fork}\">{fork}</a>\n",
"other": "{event} in <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"repos_header": "<b>Top repositories by stars</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"repo_line": "⭐ <b>{stars}</b> | <a href=\"{url}\">{name}</a> — {desc}\nLanguage: {lang} | Forks: {forks}\n\n",
"orgs_header": "<b>Organizations</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"org_line": "• <a href=\"{url}\">{login}</a> — {desc}\n",
"contrib_header": "<b>Contribution graph (last year)</b> <a href=\"https://github.com/{username}\">{username}</a>\n",
"contrib_footer": "\n⬛ = 0, 🟩 = 1+ contributions",
} }
strings_ru = { strings_ru = {
"_cls_doc": "Информация о GitHub-пользователе", "no_username": "❗ Укажи имя пользователя GitHub.",
"no_username": "❗ Укажи GitHub username.",
"user_not_found": "🚫 Пользователь не найден: <b>{}</b>", "user_not_found": "🚫 Пользователь не найден: <b>{}</b>",
"api_error": "⚠ Ошибка GitHub API: <b>{msg}</b>", "profile": "Профиль",
"no_activity": "🕸 Нет недавней активности у <b>{}</b>", "no_activity": "🕸 Нет активности у <b>{}</b>",
"no_contrib": "📭 Нет данных о контрибуциях.", "no_contrib": "📭 Нет данных о вкладах <b>{}</b>",
"no_repos": "📭 Нет публичных репозиториев.", "info_text": (
"no_orgs": "📭 Не состоит в организациях.", "👤 <b>{name}</b> | <a href=\"{url}\">{profile}</a>\n"
"no_title": "Без названия", "🏢 {company} | 📍 {location}\n"
"no_desc": "Без описания", "📝 {bio}\n\n"
"not_specified": "Не указано", "📦 Репозитории: <b>{repos}</b> | "
"more_commits": " ... и ещё {}\n", "👥 Подписчики: <b>{followers}</b> | "
"hireable_yes": "Да",
"hireable_no": "Нет",
"menu_text": "Выбери раздел:",
"btn_activity": "🔥 Активность",
"btn_contrib": "📊 Контрибы",
"btn_repos": "📦 Репозитории",
"btn_orgs": "🏛 Организации",
"btn_back": "← Назад к профилю",
"profile_header": "<b>Профиль</b> <a href=\"{url}\">{username}</a>\n\n",
"profile_text": (
"👤 Имя: <b>{name}</b>\n"
"🏷 Логин: <code>{login}</code>\n"
"📝 Био: {bio}\n"
"🏢 Компания: {company}\n"
"📍 Локация: {location}\n"
"📧 Email: {email}\n"
"🔗 Сайт: {blog}\n"
"🐦 Twitter: {twitter}\n"
"💼 Доступен для найма: {hireable}\n"
"📊 Тип аккаунта: {type}\n"
"📦 Публичные репозитории: <b>{repos}</b>\n"
"⭐ Публичные гисты: <b>{gists}</b>\n"
"👥 Подписчики: <b>{followers}</b>\n"
"👣 Подписки: <b>{following}</b>\n" "👣 Подписки: <b>{following}</b>\n"
"🕐 Создан: <code>{created}</code>\n" "🕒 Создан: <code>{created}</code>"
"🕐 Обновлён: <code>{updated}</code>"
), ),
"activity_header": "<b>Последняя активность:</b>\n",
"activity_header": "<b>Последняя активность</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n", "activity_commit": "🔨 {count} коммит(ов) → <code>{branch}</code> в {repo}",
"activity_create": "✨ Создан {ref_type} в {repo}",
"push_header": "🔨 Запушил в <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_pr": "🔄 {action} PR: {title}",
"push_no_commits": "🔨 Запушил (без деталей) в <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_issue": "{action} issue: {title}",
"commit_line": "• <a href=\"{url}\"><code>{sha}</code></a>: {message}\n", "activity_star": "В избранное {repo}",
"activity_fork": "⑂ Форк в {fork}",
"create_branch": "✨ Создал ветку <code>{ref}</code> в <a href=\"https://github.com/{repo}\">{repo}</a>\n", "activity_other": "{event} в {repo}",
"create_tag": "✨ Создал тег <code>{ref}</code> в <a href=\"https://github.com/{repo}/releases/tag/{ref}\">{repo}</a>\n", "contrib_header": "<b>График активности</b> <a href=\"https://github.com/{username}\">{username}</a>:\n",
"create_repo": "✨ Создал репозиторий <a href=\"https://github.com/{repo}\">{repo}</a>\n", "contrib_footer": "⬛ = 0, 🟩 = 1+ контрибуций",
"pr_opened": "🔄 Открыл PR <a href=\"{url}\">#{} {title}</a>\n",
"pr_closed": "🔄 Закрыл PR <a href=\"{url}\">#{} {title}</a>\n",
"pr_merged": "🔄 Замержил PR <a href=\"{url}\">#{} {title}</a>\n",
"issue_opened": "❗ Открыл issue <a href=\"{url}\">#{} {title}</a>\n",
"issue_closed": "❗ Закрыл issue <a href=\"{url}\">#{} {title}</a>\n",
"star": "⭐ Добавил в избранное <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"fork": "⑂ Форкнул <a href=\"https://github.com/{fork}\">{fork}</a>\n",
"other": "{event} в <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"repos_header": "<b>Топ репозитории по звёздам</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"repo_line": "⭐ <b>{stars}</b> | <a href=\"{url}\">{name}</a> — {desc}\nЯзык: {lang} | Форков: {forks}\n\n",
"orgs_header": "<b>Организации</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"org_line": "• <a href=\"{url}\">{login}</a> — {desc}\n",
"contrib_header": "<b>График контрибуций (последний год)</b> <a href=\"https://github.com/{username}\">{username}</a>\n",
"contrib_footer": "\n⬛ = 0, 🟩 = 1+ контрибуций",
} }
def __init__(self): def __init__(self):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
async def github_fetch(self, url, github_api=True): def github_api(self, url):
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36",
"Accept": "application/vnd.github+json" if github_api else "application/json",
"X-GitHub-Api-Version": "2022-11-28",
}
req = urllib.request.Request(url, headers=headers)
try: try:
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(url) as resp:
raw = resp.read().decode("utf-8") return json.loads(resp.read().decode())
return json.loads(raw) if raw else {}
except Exception as e: except Exception as e:
self.logger.error(f"[GitHub] {e}") self.logger.warning(f"[GitHub API] {e}")
return {"message": str(e)} return None
@loader.command(ru_doc="{username без @} — Информация о GitHub пользователе") def get_username(self, message):
async def github(self, message): args = message.text.split(maxsplit=1)
"""{username without @} — GitHub user information""" return args[1] if len(args) > 1 else None
username = utils.get_args_raw(message)
@loader.command(doc="Show GitHub user info", ru_doc="Информация о пользователе GitHub")
async def gh(self, message):
"""Show GitHub user info"""
username = self.get_username(message)
if not username: if not username:
await utils.answer(message, self.strings("no_username")) return await message.edit(self.strings("no_username"))
return
user_data = await self.github_fetch(f"https://api.github.com/users/{username}") data = self.github_api(f"https://api.github.com/users/{username}")
if "message" in user_data: if not data:
await utils.answer(message, self.strings("user_not_found").format(username)) return await message.edit(self.strings("user_not_found").format(username))
return
hireable = self.strings("hireable_yes") if user_data.get("hireable") else self.strings("hireable_no") await message.edit(self.strings("info_text").format(
name=data.get("name") or username,
url=data["html_url"],
profile=self.strings("profile"),
company=data.get("company", "N/A"),
location=data.get("location", "N/A"),
bio=data.get("bio", "No bio"),
repos=data.get("public_repos", 0),
followers=data.get("followers", 0),
following=data.get("following", 0),
created=data.get("created_at", "")[:10]
))
profile_text = ( @loader.command(doc="Show recent GitHub activity", ru_doc="Последняя активность GitHub")
self.strings("profile_header").format(url=user_data["html_url"], username=username) async def gha(self, message):
+ self.strings("profile_text").format( """Show recent GitHub activity"""
name=user_data.get("name") or self.strings("not_specified"), username = self.get_username(message)
login=username, if not username:
bio=user_data.get("bio") or self.strings("no_desc"), return await message.edit(self.strings("no_username"))
company=user_data.get("company") or self.strings("not_specified"),
location=user_data.get("location") or self.strings("not_specified"),
email=user_data.get("email") or self.strings("not_specified"),
blog=user_data.get("blog") or self.strings("not_specified"),
twitter=user_data.get("twitter_username") or self.strings("not_specified"),
hireable=hireable,
type=user_data.get("type", "User"),
repos=user_data.get("public_repos", 0),
gists=user_data.get("public_gists", 0),
followers=user_data.get("followers", 0),
following=user_data.get("following", 0),
created=user_data.get("created_at", "")[:10],
updated=user_data.get("updated_at", "")[:10],
)
+ "\n" + self.strings("menu_text")
)
await self.inline.form( events = self.github_api(f"https://api.github.com/users/{username}/events?per_page=5")
message=message,
text=profile_text,
reply_markup=[
[{"text": self.strings("btn_activity"), "callback": self._activity, "args": (username,)}],
[{"text": self.strings("btn_contrib"), "callback": self._contrib, "args": (username,)}, {"text": self.strings("btn_repos"), "callback": self._repos, "args": (username,)}],
[{"text": self.strings("btn_orgs"), "callback": self._orgs, "args": (username,)}],
],
ttl=10 * 60,
)
async def _profile(self, call: InlineCall, username: str):
# Этот метод теперь используется только для возврата к профилю
data = await self.github_fetch(f"https://api.github.com/users/{username}")
if "message" in data:
await call.edit(self.strings("api_error").format(msg=data["message"]))
return
hireable = self.strings("hireable_yes") if data.get("hireable") else self.strings("hireable_no")
profile_text = (
self.strings("profile_header").format(url=data["html_url"], username=username)
+ self.strings("profile_text").format(
name=data.get("name") or self.strings("not_specified"),
login=username,
bio=data.get("bio") or self.strings("no_desc"),
company=data.get("company") or self.strings("not_specified"),
location=data.get("location") or self.strings("not_specified"),
email=data.get("email") or self.strings("not_specified"),
blog=data.get("blog") or self.strings("not_specified"),
twitter=data.get("twitter_username") or self.strings("not_specified"),
hireable=hireable,
type=data.get("type", "User"),
repos=data.get("public_repos", 0),
gists=data.get("public_gists", 0),
followers=data.get("followers", 0),
following=data.get("following", 0),
created=data.get("created_at", "")[:10],
updated=data.get("updated_at", "")[:10],
)
+ "\n" + self.strings("menu_text")
)
await call.edit(
text=profile_text,
reply_markup=[
[{"text": self.strings("btn_activity"), "callback": self._activity, "args": (username,)}],
[{"text": self.strings("btn_contrib"), "callback": self._contrib, "args": (username,)}, {"text": self.strings("btn_repos"), "callback": self._repos, "args": (username,)}],
[{"text": self.strings("btn_orgs"), "callback": self._orgs, "args": (username,)}],
]
)
async def _activity(self, call: InlineCall, username: str):
events = await self.github_fetch(f"https://api.github.com/users/{username}/events?per_page=40")
if "message" in events:
await call.edit(self.strings("api_error").format(msg=events["message"]), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
if not events: if not events:
await call.edit(self.strings("no_activity").format(username=username), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]) return await message.edit(self.strings("no_activity").format(username))
return
lines = [self.strings("activity_header").format(username=username)] lines = []
seen_repos = set() for event in events:
for event in events[:25]:
etype = event["type"] etype = event["type"]
repo = event["repo"]["name"] repo = event["repo"]["name"]
if repo in seen_repos and len(lines) > 20:
continue
payload = event.get("payload", {}) payload = event.get("payload", {})
if etype == "PushEvent": if etype == "PushEvent":
branch = payload.get("ref", "refs/heads/main").replace("refs/heads/", "") branch = re.sub(r"refs/heads/", "", payload.get("ref", "main"))
commits = payload.get("commits", []) count = len(payload.get("commits", []))
lines.append(self.strings("activity_commit").format(count=count, branch=branch, repo=repo))
if commits:
lines.append(self.strings("push_header").format(branch=branch, repo=repo))
for commit in commits[:5]:
sha = commit["sha"][:7]
message = commit["message"].split("\n")[0][:100]
if len(commit["message"].split("\n")[0]) > 100:
message += "..."
url = f"https://github.com/{repo}/commit/{commit['sha']}"
lines.append(self.strings("commit_line").format(url=url, sha=sha, message=message))
if len(commits) > 5:
lines.append(self.strings("more_commits").format(len(commits)-5))
else:
lines.append(self.strings("push_no_commits").format(branch=branch, repo=repo))
seen_repos.add(repo)
elif etype == "CreateEvent": elif etype == "CreateEvent":
ref_type = payload.get("ref_type") lines.append(self.strings("activity_create").format(ref_type=payload.get("ref_type"), repo=repo))
ref = payload.get("ref") or ""
if ref_type == "branch":
lines.append(self.strings("create_branch").format(ref=ref, repo=repo))
elif ref_type == "tag":
lines.append(self.strings("create_tag").format(ref=ref, repo=repo))
elif ref_type == "repository":
lines.append(self.strings("create_repo").format(repo=repo))
elif etype == "PullRequestEvent": elif etype == "PullRequestEvent":
pr = payload.get("pull_request", {}) pr = payload.get("pull_request", {})
number = pr.get("number", "?") lines.append(self.strings("activity_pr").format(action=payload.get("action"), title=pr.get("title")))
title = pr.get("title") or self.strings("no_title")
url = pr.get("html_url") or f"https://github.com/{repo}"
action = payload.get("action")
if action == "closed" and pr.get("merged"):
lines.append(self.strings("pr_merged").format(url=url, number=number, title=title))
elif action == "opened":
lines.append(self.strings("pr_opened").format(url=url, number=number, title=title))
elif action == "closed":
lines.append(self.strings("pr_closed").format(url=url, number=number, title=title))
elif etype == "IssuesEvent": elif etype == "IssuesEvent":
issue = payload.get("issue", {}) issue = payload.get("issue", {})
number = issue.get("number", "?") lines.append(self.strings("activity_issue").format(action=payload.get("action"), title=issue.get("title")))
title = issue.get("title") or self.strings("no_title")
url = issue.get("html_url") or f"https://github.com/{repo}"
action = payload.get("action")
if action == "opened":
lines.append(self.strings("issue_opened").format(url=url, number=number, title=title))
elif action == "closed":
lines.append(self.strings("issue_closed").format(url=url, number=number, title=title))
elif etype == "WatchEvent": elif etype == "WatchEvent":
lines.append(self.strings("star").format(repo=repo)) lines.append(self.strings("activity_star").format(repo=repo))
elif etype == "ForkEvent": elif etype == "ForkEvent":
fork = payload.get("forkee", {}).get("full_name", "unknown") lines.append(self.strings("activity_fork").format(fork=payload.get("forkee", {}).get("full_name")))
lines.append(self.strings("fork").format(fork=fork))
else: else:
event_name = etype.replace("Event", "") lines.append(self.strings("activity_other").format(event=etype, repo=repo))
lines.append(self.strings("other").format(event=event_name, repo=repo))
await call.edit( await message.edit(self.strings("activity_header") + "\n".join(lines))
text="".join(lines),
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
)
async def _repos(self, call: InlineCall, username: str): @loader.command(doc="Show GitHub contribution graph", ru_doc="Показать график контрибов GitHub")
repos = await self.github_fetch(f"https://api.github.com/users/{username}/repos?sort=stars&per_page=10") async def ghc(self, message):
if "message" in repos: """Show GitHub contribution graph"""
await call.edit(self.strings("api_error").format(msg=repos["message"]), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]) username = self.get_username(message)
return if not username:
if not repos: return await message.edit(self.strings("no_username"))
await call.edit(self.strings("no_repos"), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
lines = [self.strings("repos_header").format(username=username)] data = self.github_api(f"https://github-contributions-api.deno.dev/{username}.json")
for repo in repos[:10]: contribs = data.get("contributions") if data else None
lines.append(self.strings("repo_line").format(
stars=repo.get("stargazers_count", 0),
url=repo["html_url"],
name=repo["name"],
desc=repo.get("description") or self.strings("no_desc"),
lang=repo.get("language") or self.strings("not_specified"),
forks=repo.get("forks_count", 0),
))
await call.edit( if not isinstance(contribs, list):
text="".join(lines), return await message.edit(self.strings("no_contrib").format(username))
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
)
async def _orgs(self, call: InlineCall, username: str):
orgs = await self.github_fetch(f"https://api.github.com/users/{username}/orgs")
if "message" in orgs:
await call.edit(self.strings("api_error").format(msg=orgs["message"]), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
if not orgs:
await call.edit(self.strings("no_orgs"), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
lines = [self.strings("orgs_header").format(username=username)]
for org in orgs:
lines.append(self.strings("org_line").format(
url=f"https://github.com/{org['login']}",
login=org["login"],
desc=org.get("description") or self.strings("no_desc"),
))
await call.edit(
text="".join(lines),
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
)
async def _contrib(self, call: InlineCall, username: str):
data = await self.github_fetch(f"https://github-contributions-api.deno.dev/{username}.json", github_api=False)
if not data or not data.get("contributions"):
await call.edit(self.strings("no_contrib"), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
raw_days = []
for week in data.get("contributions", []):
if isinstance(week, list):
raw_days.extend([day for day in week if isinstance(day, dict)])
today = datetime.utcnow().date() today = datetime.utcnow().date()
weeks_count = 53 start = today - timedelta(days=90)
days_back = weeks_count * 7 + 7 matrix = [["" for _ in range(13)] for _ in range(7)]
start = today - timedelta(days=days_back)
matrix = [["" for _ in range(weeks_count)] for _ in range(7)] for entry in contribs:
for entry in raw_days:
date_str = entry.get("date")
if not date_str:
continue
try: try:
date = datetime.strptime(date_str, "%Y-%m-%d").date() date = datetime.strptime(entry["date"], "%Y-%m-%d").date()
if date < start or date > today: if not (start <= date <= today):
continue continue
count = entry.get("contributionCount") or entry.get("count", 0) or 0 day = (date.weekday() + 1) % 7 # Sunday=0
if count > 0: week = (date - start).days // 7
day_idx = (date.weekday() + 1) % 7 if entry.get("contributionCount", 0) > 0:
week_idx = (date - start).days // 7 matrix[day][week] = "🟩"
if week_idx < weeks_count: except:
matrix[day_idx][week_idx] = "🟩"
except Exception:
continue continue
days_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
graph = "\n".join(f"{days_labels[i]} {''.join(matrix[i])}" for i in range(7)) graph = "\n".join(f"{days[i]} {''.join(matrix[i])}" for i in range(7))
await call.edit( await message.edit(
text=self.strings("contrib_header").format(username=username) self.strings("contrib_header").format(username=username)
+ f"<pre>{graph}</pre>" + f"<pre>{graph}</pre>\n"
+ self.strings("contrib_footer"), + self.strings("contrib_footer")
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
) )

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, auto, restart, heroku, hikka
from hikkatl.types import Message from hikkatl.types import Message
from .. import loader, utils from .. import loader, utils

View File

@@ -2,7 +2,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @PyModule # meta developer: @PyModule
# meta fhsdesc: fun, rp, rpgame
# requires: toml # requires: toml
import os import os
from hikka import loader, utils from hikka import loader, utils

View File

@@ -2,7 +2,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @PyModule # meta developer: @PyModule
# meta fhsdesc: tool, tools, lyrics, music
import requests import requests
from bs4 import BeautifulSoup, Tag, NavigableString from bs4 import BeautifulSoup, Tag, NavigableString
import re import re

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, minecraft, game
import aiohttp import aiohttp
import base64 import base64

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, in heroku
# requires: asyncio # requires: asyncio
from .. import loader, utils from .. import loader, utils

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, point, auto
from .. import loader, utils from .. import loader, utils

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, qr
from .. import loader, utils from .. import loader, utils
import requests import requests

View File

@@ -3,7 +3,6 @@
# scope: hikka_only # scope: hikka_only
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, random
from .. import loader, utils from .. import loader, utils
import random import random

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, test, speedtest
# requires: speedtest-cli # requires: speedtest-cli
import speedtest import speedtest

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, info, sysinfo, system
# requires: psutil # requires: psutil
from .. import loader, utils from .. import loader, utils

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0 # https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule # meta developer: @pymodule
# meta fhsdesc: tool, tools, admin, tag, alltag, tagall
from .. import loader, utils from .. import loader, utils
from telethon.tl.types import ChannelParticipantsAdmins, UserStatusRecently, UserStatusOnline, Message from telethon.tl.types import ChannelParticipantsAdmins, UserStatusRecently, UserStatusOnline, Message

View File

@@ -5,7 +5,6 @@
# Name: UserParser # Name: UserParser
# Description: Данный модуль позволяет копировать ID, Username и Name участников чата при помощи команды .userpars # Description: Данный модуль позволяет копировать ID, Username и Name участников чата при помощи команды .userpars
# meta developer: @PyModule # meta developer: @PyModule
# meta fhsdesc: tool, tools, id, parser, userparser
from .. import loader, utils from .. import loader, utils
import json import json

View File

@@ -12,7 +12,6 @@
# meta developer: @pymodule # meta developer: @pymodule
# requires: aiohttp # requires: aiohttp
# meta fhsdesc: tool, tools, wiki, wikipedia, info, wikiinfo
from .. import loader, utils from .. import loader, utils
from ..inline.types import InlineQuery from ..inline.types import InlineQuery

125359
modules.json

File diff suppressed because one or more lines are too long

456
parse.py
View File

@@ -1,353 +1,171 @@
import os
import ast import ast
import json import json
import os
import logging
from typing import Dict, Any, Optional, List
logging.basicConfig(level=logging.WARNING, format="%(message)s") from clone_repos import repos
logger = logging.getLogger(__name__) from typing import Dict
def safe_unparse(node: ast.AST) -> str: # TODO: ADD VENV IGNORE
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 = {}
for i in repositories: def get_module_info(module_path):
path = i.get("path", "") """Парсит Python-модуль и извлекает информацию о нем."""
blacklist = i.get("blacklist", []) with open(module_path, "r", encoding="utf-8") as f:
if path and blacklist: module_content = f.read()
blacklisted_modules[path] = blacklist
return blacklisted_modules meta_info = {"pic": None, "banner": None}
for line in module_content.split("\n"):
if line.startswith("# meta"):
key, value = line.replace("# meta ", "").split(": ")
meta_info[key] = value
def extract_string_value(node: ast.AST) -> Optional[str]: tree = ast.parse(module_content)
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 extract_loader_command_args(decorator: ast.Call) -> Dict[str, Any]: def get_decorator_names(decorator_list):
args = {"lang_docs": {}, "aliases": [], "usage": None} return [ast.unparse(decorator) for decorator in decorator_list]
try:
for kw in decorator.keywords:
arg_name = kw.arg
if not arg_name:
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
def get_module_info(module_path: str) -> Optional[Dict[str, Any]]: def extract_loader_command_args(decorator):
try: """Извлекает аргументы `ru_doc` и `en_doc` из `@loader.command`."""
with open(module_path, "r", encoding="utf-8") as f: if (
source = f.read() isinstance(decorator, ast.Call)
except Exception as e: and hasattr(decorator.func, "attr")
logger.warning(f"Skipping {module_path}: read failed — {e}") and decorator.func.attr == "command"
return None ):
ru_doc = None
source = source.lstrip('\ufeff') en_doc = None
source = ''.join(c for c in source if ord(c) >= 32 or c in '\n\r\t') if source else source for keyword in decorator.keywords:
if keyword.arg == "ru_doc":
meta = {"pic": None, "banner": None, "developer": None} ru_doc = ast.literal_eval(keyword.value)
for line in source.splitlines(): elif keyword.arg == "en_doc":
line = line.strip() en_doc = ast.literal_eval(keyword.value)
if line.startswith("# meta "): return ru_doc, en_doc
try: return None, None
key, val = line[len("# meta "):].split(":", 1)
meta[key.strip()] = val.strip()
except ValueError:
pass
try:
tree = ast.parse(source, filename=module_path)
except SyntaxError as e:
logger.warning(f"Skipping {module_path}: syntax error — {e}")
return {
"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": {},
}
module_data = None
result = {}
for node in ast.walk(tree): for node in ast.walk(tree):
if not isinstance(node, ast.ClassDef): if isinstance(node, ast.ClassDef):
continue decorators = get_decorator_names(node.decorator_list)
is_tds_mod = [d for d in decorators if "loader.tds" in d]
is_module_class = ( if "Mod" not in node.name and not is_tds_mod:
"Mod" in node.name or
any(isinstance(d, ast.Attribute) and safe_unparse(d).startswith("loader.tds") for d in node.decorator_list) or
any(isinstance(d, ast.Name) and d.id == "loader" for d in node.decorator_list)
)
if not is_module_class:
continue
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 continue
name = func.name class_docstring = ast.get_docstring(node)
if name == "on_load": class_info = {
info["has_on_load"] = True "name": node.name,
continue "description": class_docstring,
if name == "on_unload": "meta": meta_info,
info["has_on_unload"] = True "commands": [],
continue "new_commands": [],
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: for class_body_node in node.body:
if isinstance(dec, ast.Call) and hasattr(dec.func, 'attr'): if isinstance(class_body_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
attr = dec.func.attr decorators = get_decorator_names(class_body_node.decorator_list)
if attr == "command": is_loader_command = [d for d in decorators if "command" in d]
cmd.update(extract_loader_command_args(dec)) if not is_loader_command and "cmd" not in class_body_node.name:
elif attr == "inline_handler": continue
cmd["inline"] = True
cmd["is_inline_handler"] = True
elif attr in ("unrestricted", "owner", "support"):
cmd["decorators"].append(attr)
for stmt in func.body: method_docstring = ast.get_docstring(class_body_node)
if isinstance(stmt, ast.Assign): command_name = class_body_node.name
for target in stmt.targets: ru_doc, en_doc = None, None
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("__") for decorator in class_body_node.decorator_list:
if not (is_decorated or is_command_name): ru_doc_tmp, en_doc_tmp = extract_loader_command_args(decorator)
continue if ru_doc_tmp:
ru_doc = ru_doc_tmp
if en_doc_tmp:
en_doc = en_doc_tmp
clean_name = cmd["name"].replace("cmd", "").replace("_", "") descriptions = []
if method_docstring:
descs = [] descriptions.append(method_docstring)
legacy_key = f"_cmd_doc_{clean_name}" if ru_doc:
legacy_doc = info["strings"].get(legacy_key) descriptions.append(ru_doc)
base_doc = legacy_doc if legacy_doc else cmd["doc"] if en_doc:
if base_doc: descriptions.append(en_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)) class_info["commands"].append(
info["commands"].append({clean_name: full_desc}) {command_name: ' '.join(descriptions)}
)
desc_map = {"default": legacy_doc or cmd["doc"]} command_name = command_name.replace('cmd', '')
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({ class_info["new_commands"].append(
"name": clean_name, {
"original_name": cmd["name"], command_name: {
"description": desc_map, "ru_doc": ru_doc,
"cmd_names": cmd["cmd_names"], "en_doc": en_doc,
"aliases": cmd["aliases"], "doc": method_docstring,
"usage": cmd["usage"], }
"inline": cmd["inline"], }
"is_inline_handler": cmd["is_inline_handler"], )
"decorators": cmd["decorators"],
})
if cmd["is_inline_handler"]: result = class_info
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 return result
break
return module_data def parse_developers(base_dir: str) -> Dict[str, list]:
developers = {
def main(): "repo": set(), # используем set внутри функции
base_dir = os.getcwd() "channel": set()
modules = {} }
blacklisted_modules = load_blacklist("repositories.json")
for repo_url in repos:
for root, dirs, files in os.walk(base_dir): repo_path = repo_url.replace("https://github.com/", "")
dirs[:] = [d for d in dirs if d not in ("venv", ".venv", "env", ".env", ".git")] try:
owner, repo_name = repo_path.split("/")
developers["repo"].add(owner)
except ValueError:
print(f"Incorrect URL of repository: {repo_url}")
continue
for root, _, files in os.walk(base_dir):
for file in files: 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), []): if file.endswith(".py"):
path = os.path.join(root, file) file_path = os.path.join(root, file)
try: try:
data = get_module_info(path) module_info = get_module_info(file_path)
if data: if module_info and "meta" in module_info:
rel = os.path.relpath(path, base_dir).replace("\\", "/") developer = module_info["meta"].get('developer')
modules[rel] = data if developer: # Проверяем, что developer не None
# Разделяем строки с запятыми, &, | и пробелами
for dev in developer.replace(',', ' ').replace('&', ' ').replace('|', ' ').split():
# Добавляем только элементы, начинающиеся с @
if dev.startswith('@'):
developers["channel"].add(dev.strip())
except Exception as e: except Exception as e:
logger.error(f"Error processing {path}: {e}") print(f"Ошибка при парсинге файла {file_path}: {e}")
output = { # Преобразуем set в list перед возвратом
"modules": modules, return {
"meta": { "repo": list(developers["repo"]),
"total_modules": len(modules), "channel": list(developers["channel"])
"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)
print(f"modules.json written ({len(modules)} modules)") modules_data = {}
base_dir = os.getcwd()
if __name__ == "__main__": for root, _, files in os.walk(base_dir):
main() for file in files:
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 создан!")

View File

@@ -1,239 +0,0 @@
{
"repositories": [
{
"url": "https://github.com/DziruModules/hikkamods",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/kamolgks/Hikkamods",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/thomasmod/hikkamods",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/SkillsAngels/Modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Sad0ff/modules-ftg",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Yahikoro/Modules-for-FTG",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/KeyZenD/modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/AlpacaGang/ftg-modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/trololo65/Modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Ijidishurka/modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Fl1yd/FTG-Modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/D4n13l3k00/FTG-Modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/iamnalinor/FTG-modules",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/SekaiYoneya/modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/GeekTG/FTG-Modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Den4ikSuperOstryyPer4ik/Astro-modules",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/vsecoder/hikka_modules",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/sqlmerr/hikka_mods",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/N3rcy/modules",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/KorenbZla/HikkaModules",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/MuRuLOSE/HikkaModulesRepo",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/coddrago/modules",
"tags": ["herokutrusted", "hikkatrusted"],
"blacklist": []
},
{
"url": "https://github.com/1jpshiro/hikka-modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/MoriSummerz/ftg-mods",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/anon97945/hikka-mods",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/dorotorothequickend/DorotoroModules",
"tags": ["hikkatrusted", "nonlongermaintained"],
"blacklist": []
},
{
"url": "https://github.com/AmoreForever/amoremods",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/idiotcoders/idiotmodules",
"tags": ["hikkatrusted", "herokutrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/CakesTwix/Hikka-Modules",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/archquise/H.Modules",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/GD-alt/mm-hikka-mods",
"tags": ["hikkatrusted", "herokutrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/HitaloSama/FTG-modules-repo",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/SekaiYoneya/Friendly-telegram",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/blazedzn/ftg-modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/hikariatama/ftg",
"tags": ["hikkatrusted", "nonactive"],
"blacklist": []
},
{
"url": "https://github.com/m4xx1m/FTG",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/skillzmeow/skillzmods_hikka",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/fajox1/famods",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/TheKsenon/MyHikkaModules",
"tags": ["hikkatrusted", "herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/cryptexctl/modules-mirror",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/Ruslan-Isaev/modules",
"tags": ["herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/shadowhikka/sh.modules",
"tags": [],
"blacklist": []
},
{
"url": "https://github.com/fiksofficial/python-modules",
"tags": ["herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/mead0wsss/mead0wsMods",
"tags": ["herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/SenkoGuardian/SenModules",
"tags": ["herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/ZetGoHack/nullmod",
"tags": ["herokutrusted"],
"blacklist": []
},
{
"url": "https://github.com/yummy1gay/limoka",
"tags": [],
"blacklist": []
}
]
}

20
trusted.json Normal file
View File

@@ -0,0 +1,20 @@
{
"trusted": [
"iamnalinor/FTG-modules",
"vsecoder/hikka_modules",
"sqlmerr/hikka_mods",
"N3rcy/modules",
"KorenbZla/HikkaModules",
"coddrago/modules",
"MoriSummerz/ftg-mods",
"anon97945/hikka-mods",
"dorotorothequickend/DorotoroModules",
"idiotcoders/idiotmodules",
"C0dwiz/H.Modules",
"GD-alt/mm-hikka-mods",
"hikariatama/ftg",
"fajox1/famods",
"TheKsenon/MyHikkaModules",
"Den4ikSuperOstryyPer4ik/Astro-modules"
]
}

View File

@@ -1,193 +0,0 @@
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())