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
44 changed files with 45340 additions and 84208 deletions

View File

@@ -10,10 +10,8 @@ on:
pull_request:
branches:
- main
types: [opened, synchronize, reopened, closed]
workflow_dispatch: # Allows manual triggering from GitHub UI
# Environment variables available to all jobs
env:
BRANCH_NAME: "update-submodules_${{ github.sha }}"
@@ -139,8 +137,9 @@ jobs:
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install requests
pip install requests scikit-learn tqdm
python3 parse.py
python3 categories.py
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 remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${REPO_URL}"
@@ -207,54 +206,61 @@ jobs:
echo "Branch ${{ env.BRANCH_NAME }} does not exist in remote repository, skipping PR creation."
fi
notify_diffs:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: parse
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Git
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 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: |
LAST_COMMIT_MESSAGE=$(git log -1 --pretty=%B)
if [[ "$LAST_COMMIT_MESSAGE" == Merge* ]]; then
python3 update_diffs.py --token ${{ secrets.TELEGRAM_BOT_TOKEN }} --chat_id ${{ secrets.TELEGRAM_CHAT_ID_UPDATE }} --base_commit HEAD~1
fi
backup:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
needs: parse
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
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.GIT_DEPTH }}
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install Python dependencies
run: pip install aiohttp
- name: Run backup script
- name: Create and send backup to Telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
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

@@ -23,4 +23,4 @@ class K(loader.Module):
"""K"""
raise Exception("Testing error handling")
await utils.answer(message, "K")
# why FUCK YOU BILL GATES ЛЛЛЛ
# why

View File

@@ -3,7 +3,9 @@
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
__version__ = (5, 8, 1) #фыр
__version__ = (5, 7, 0) #перепешите на меня квартиру пж
#ладно
# meta developer: @SenkoGuardianModules
@@ -19,14 +21,10 @@ import os
import io
import random
import socket
import base64
import uuid
import json
from PIL import Image
import asyncio
import logging
import tempfile
import aiohttp
import httpx
from datetime import datetime
from markdown_it import MarkdownIt
import pytz
@@ -61,13 +59,11 @@ DB_GAUTO_HISTORY_KEY = "gemini_gauto_conversations_v1"
DB_IMPERSONATION_KEY = "gemini_impersonation_chats"
GEMINI_TIMEOUT = 840
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
class Gemini(loader.Module):
"""Модуль для работы с Google Gemini AI. (Поддержка видео/фото/аудио"""
"""Модуль для работы с Google Gemini AI (New SDK). Поддержка видео/фото/аудио и контекста пользователей."""
strings = {
"name": "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_temperature_doc": "Температура генерации (креативность). От 0.0 до 2.0. По умолчанию 1.0.",
"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>',
"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>",
@@ -141,7 +135,7 @@ class Gemini(loader.Module):
"gme_sent_to_saved": "💾 История экспортирована в избранное.",
"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_updated": "✅ <b>Системный промпт обновлен!</b>\nДлина: {} символов.",
"gprompt_updated": "✅ <b>Системный промпт обновлен!</b>\nДлина: {} симв.",
"gprompt_cleared": "🗑 <b>Системный промпт очищен.</b>",
"gprompt_current": "📝 <b>Текущий системный промпт:</b>",
"gprompt_file_error": "❗️ <b>Ошибка чтения файла:</b> {}",
@@ -149,7 +143,6 @@ class Gemini(loader.Module):
"gprompt_not_text": "❗️ Это не похоже на текстовый файл.(txt)",
"gmodel_no_models": "⚠️ Не удалось получить список моделей.",
"gmodel_list_error": "❗️ Ошибка получения списка: {}",
"gimg_process": "<emoji document_id=5325547803936572038>✨</emoji> <b>Генерация...</b>\n🧠 <i>Модель: {model}</i>",
}
TEXT_MIME_TYPES = {
"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("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("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.gauto_conversations = {}
@@ -190,25 +181,16 @@ class Gemini(loader.Module):
self.impersonation_chats = set()
self._lock = asyncio.Lock()
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):
self.client = client
self.db = db
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:
logger.error("Gemini: 'google-genai' library missing! pip install google-genai")
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.conversations = self._load_history_from_db(DB_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)
if regeneration and raw_hist: raw_hist = raw_hist[:-2]
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)
if not impersonation_mode:
try: user_timezone = pytz.timezone(self.config["timezone"])
except pytz.UnknownTimeZoneError: user_timezone = pytz.utc
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):
request_parts[0] = types.Part(text=f"{time_note}\n\n{request_parts[0].text}")
else:
@@ -382,19 +367,22 @@ class Gemini(loader.Module):
http_opts = None
if proxy_config:
http_opts = types.HttpOptions(async_client_args={"proxies": proxy_config})
client = genai.Client(api_key=api_key, http_options=http_opts)
response = await client.aio.models.generate_content(
model=self.config["model_name"],
contents=contents,
config=gen_config
)
if response.text:
result_text = response.text
was_successful = True
if self.config["google_search"]: search_icon = " 🌐"
self.current_api_key_index = current_idx
break
else: raise ValueError("Empty response")
else:
raise ValueError("Empty response (Safety?)")
except Exception as e:
err_str = str(e).lower()
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
break
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)):
self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode)
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>"
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
is_long_text = len(result_text) > 3500
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:
if len(text_to_send) > 4096:
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:
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 call.edit(f"{self.strings['response_too_long']}", reply_markup=None)
elif status_msg:
await status_msg.delete()
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
)
@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()
async def gch(self, message: Message):
"""<[id чата]> <кол-во> <вопрос> - Проанализировать историю чата."""
@@ -573,8 +479,6 @@ class Gemini(loader.Module):
entity = await self.client.get_entity(target_chat_id)
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)
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:
return await utils.answer(status_msg, self.strings["gch_chat_error"].format(target_chat_id, e))
full_prompt = (
@@ -767,6 +671,7 @@ class Gemini(loader.Module):
import json
hist = json.loads(f)
if not isinstance(hist, list): raise ValueError
cid = utils.get_chat_id(message)
target = self.gauto_conversations if gauto else self.conversations
target[str(cid)] = hist
@@ -776,7 +681,7 @@ class Gemini(loader.Module):
@loader.command()
async def gmemfind(self, message: Message):
"""[слово] — Поиск в памяти текущего чата по ключевому слову или фразе."""
"""[слово] — Поиск по истории текущего чата по ключевому слову или фразе."""
q = utils.get_args_raw(message).lower()
if not q: return await utils.answer(message, "Укажите слово для поиска.")
cid = utils.get_chat_id(message)
@@ -850,118 +755,6 @@ class Gemini(loader.Module):
self._save_history_sync(False)
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)
async def watcher(self, message: Message):
if not hasattr(message, 'chat_id'): return
@@ -1013,13 +806,10 @@ class Gemini(loader.Module):
user_id = self.me.id
user_name = get_display_name(self.me)
message_id = getattr(message, "id", None)
if message:
try:
peer_id = get_peer_id(message)
if peer_id:
user_id = peer_id
except (TypeError, ValueError):
if message.sender_id: user_id = message.sender_id
if message.sender_id:
user_id = message.sender_id
if 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 "[ответ на медиа]"
@@ -1048,6 +838,7 @@ class Gemini(loader.Module):
"date": now,
"user_id": None
}
history.extend([user_entry, model_entry])
limit = self.config["max_history_length"]
if limit > 0 and len(history) > limit * 2:
@@ -1062,6 +853,8 @@ class Gemini(loader.Module):
del d[str(cid)]
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):
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)
@@ -1118,34 +911,42 @@ class Gemini(loader.Module):
if txt.strip(): lines.append(f"{name}: {txt.strip()}")
except: pass
return "\n".join(reversed(lines))
def _handle_error(self, e: Exception) -> str:
logger.exception("Gemini execution error")
if isinstance(e, asyncio.TimeoutError):
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)
if "quota" in msg.lower() or "429" in msg: return self.strings["all_keys_exhausted"].format(len(self.api_keys))
return self.strings["generic_error"].format(utils.escape_html(msg))
if "quota" in msg.lower() or "exhausted" in msg.lower() or "429" in 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 heading_replacer(match):
@@ -1200,7 +1001,24 @@ class Gemini(loader.Module):
async def _clear_callback(self, call: InlineCall, chat_id: int):
self._clear_history(chat_id, gauto=False)
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:
history_limit = count or self.config["impersonation_history_limit"]
@@ -1231,111 +1049,6 @@ class Gemini(loader.Module):
logger.warning(f"Не удалось получить историю для авто-ответа: {e}")
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 _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))

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 re
import requests
import json
def parse_repos(file_path: str) -> list:
"""Parse repository URLs from a given json file."""
repos = []
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
for repo in data.get("repositories", []):
repos.append(repo["url"])
return repos
repos = parse_repos("repositories.json")
repos = [
"https://github.com/DziruModules/hikkamods",
"https://github.com/kamolgks/Hikkamods",
"https://github.com/thomasmod/hikkamods",
"https://github.com/SkillsAngels/Modules",
"https://github.com/Sad0ff/modules-ftg",
"https://github.com/Yahikoro/Modules-for-FTG",
"https://github.com/KeyZenD/modules",
"https://github.com/AlpacaGang/ftg-modules",
"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():
"""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
stats
tagwatcher
hardspam
YaMusic
hardspam

View File

@@ -1,5 +1,4 @@
# meta developer: @codrago_m
# scope: heroku_min 2.0.0
import logging
from .. import utils, loader, main
@@ -114,8 +113,15 @@ class TagWatcher(loader.Module):
validator=loader.validators.Boolean(),
),
)
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._client,
self._db,
@@ -124,10 +130,6 @@ class TagWatcher(loader.Module):
description="Here will be notifications about mentions in chats.",
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):
if self.config["custom_notif_text"]:
@@ -172,7 +174,7 @@ class TagWatcher(loader.Module):
user_id=id,
msg_content=msg_content,
reply_content=reply_content,
link=await m.link(),
link=await m.link,
)
@loader.command(
@@ -240,4 +242,4 @@ class TagWatcher(loader.Module):
message_thread_id=self._notif_topic.id,
)
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
# meta developer: @pymodule
# meta fhsdesc: tool, tools, ai, assistant
from .. import loader, utils
import aiohttp

View File

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

View File

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

View File

@@ -11,7 +11,6 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
# meta fhsdesc: tool, tools, calculator, calc
from .. import loader, utils
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
# meta developer: @PyModule
# meta fhsdesc: tool, tools, channel, admintools, admin, admintool
from telethon.tl.types import Message
from .. import loader

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,459 +11,183 @@
# https://github.com/all-licenses/GNU-General-Public-License-v3.0
# meta developer: @pymodule
# meta fhsdesc: tool, tools, github, info, inline
from .. import loader, utils
from ..inline import InlineCall
import logging
import asyncio
import urllib.request
import json
import re
import urllib.request
from datetime import datetime, timedelta
@loader.tds
class GitHubInfoMod(loader.Module):
"""GitHub user information"""
"""GitHub user info, recent activity and contribution graph"""
strings = {
"name": "GitHubInfo",
"no_username": "❗ Provide a GitHub username.",
"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_contrib": "📭 No contribution data.",
"no_repos": "📭 No public repositories.",
"no_orgs": "📭 Not a member of any organizations.",
"no_title": "No title",
"no_desc": "No description",
"not_specified": "Not specified",
"more_commits": " ... and {} more\n",
"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"
"no_contrib": "📭 No contribution data for <b>{}</b>",
"info_text": (
"👤 <b>{name}</b> | <a href=\"{url}\">{profile}</a>\n"
"🏢 {company} | 📍 {location}\n"
"📝 {bio}\n\n"
"📦 Repos: <b>{repos}</b> | "
"👥 Followers: <b>{followers}</b> | "
"👣 Following: <b>{following}</b>\n"
"🕐 Created: <code>{created}</code>\n"
"🕐 Updated: <code>{updated}</code>"
"🕒 Created: <code>{created}</code>"
),
"activity_header": "<b>Recent activity</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"push_header": "🔨 Pushed to <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"push_no_commits": "🔨 Pushed (no details) to <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"commit_line": "• <a href=\"{url}\"><code>{sha}</code></a>: {message}\n",
"create_branch": "✨ Created branch <code>{ref}</code> in <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"create_tag": "✨ Created tag <code>{ref}</code> in <a href=\"https://github.com/{repo}/releases/tag/{ref}\">{repo}</a>\n",
"create_repo": "✨ Created repository <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"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",
"activity_header": "<b>Recent activity:</b>\n",
"activity_commit": "🔨 {count} commit(s) → <code>{branch}</code> in {repo}",
"activity_create": "✨ Created {ref_type} in {repo}",
"activity_pr": "🔄 {action} PR: {title}",
"activity_issue": "{action} issue: {title}",
"activity_star": "⭐ Starred {repo}",
"activity_fork": "⑂ Forked to {fork}",
"activity_other": "{event} in {repo}",
"contrib_header": "<b>Contribution graph</b> for <a href=\"https://github.com/{username}\">{username}</a>:\n",
"contrib_footer": "⬛ = 0, 🟩 = 1+ contributions",
}
strings_ru = {
"_cls_doc": "Информация о GitHub-пользователе",
"no_username": "❗ Укажи GitHub username.",
"no_username": "❗ Укажи имя пользователя GitHub.",
"user_not_found": "🚫 Пользователь не найден: <b>{}</b>",
"api_error": "⚠ Ошибка GitHub API: <b>{msg}</b>",
"no_activity": "🕸 Нет недавней активности у <b>{}</b>",
"no_contrib": "📭 Нет данных о контрибуциях.",
"no_repos": "📭 Нет публичных репозиториев.",
"no_orgs": "📭 Не состоит в организациях.",
"no_title": "Без названия",
"no_desc": "Без описания",
"not_specified": "Не указано",
"more_commits": " ... и ещё {}\n",
"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"
"profile": "Профиль",
"no_activity": "🕸 Нет активности у <b>{}</b>",
"no_contrib": "📭 Нет данных о вкладах <b>{}</b>",
"info_text": (
"👤 <b>{name}</b> | <a href=\"{url}\">{profile}</a>\n"
"🏢 {company} | 📍 {location}\n"
"📝 {bio}\n\n"
"📦 Репозитории: <b>{repos}</b> | "
"👥 Подписчики: <b>{followers}</b> | "
"👣 Подписки: <b>{following}</b>\n"
"🕐 Создан: <code>{created}</code>\n"
"🕐 Обновлён: <code>{updated}</code>"
"🕒 Создан: <code>{created}</code>"
),
"activity_header": "<b>Последняя активность</b> <a href=\"https://github.com/{username}\">{username}</a>\n\n",
"push_header": "🔨 Запушил в <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"push_no_commits": "🔨 Запушил (без деталей) в <code>{branch}</code> → <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"commit_line": "• <a href=\"{url}\"><code>{sha}</code></a>: {message}\n",
"create_branch": "✨ Создал ветку <code>{ref}</code> в <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"create_tag": "✨ Создал тег <code>{ref}</code> в <a href=\"https://github.com/{repo}/releases/tag/{ref}\">{repo}</a>\n",
"create_repo": "✨ Создал репозиторий <a href=\"https://github.com/{repo}\">{repo}</a>\n",
"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+ контрибуций",
"activity_header": "<b>Последняя активность:</b>\n",
"activity_commit": "🔨 {count} коммит(ов) → <code>{branch}</code> в {repo}",
"activity_create": "✨ Создан {ref_type} в {repo}",
"activity_pr": "🔄 {action} PR: {title}",
"activity_issue": "{action} issue: {title}",
"activity_star": "В избранное {repo}",
"activity_fork": "⑂ Форк в {fork}",
"activity_other": "{event} в {repo}",
"contrib_header": "<b>График активности</b> <a href=\"https://github.com/{username}\">{username}</a>:\n",
"contrib_footer": "⬛ = 0, 🟩 = 1+ контрибуций",
}
def __init__(self):
self.logger = logging.getLogger(__name__)
async def github_fetch(self, url, github_api=True):
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)
def github_api(self, url):
try:
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read().decode("utf-8")
return json.loads(raw) if raw else {}
with urllib.request.urlopen(url) as resp:
return json.loads(resp.read().decode())
except Exception as e:
self.logger.error(f"[GitHub] {e}")
return {"message": str(e)}
self.logger.warning(f"[GitHub API] {e}")
return None
@loader.command(ru_doc="{username без @} — Информация о GitHub пользователе")
async def github(self, message):
"""{username without @} — GitHub user information"""
username = utils.get_args_raw(message)
def get_username(self, message):
args = message.text.split(maxsplit=1)
return args[1] if len(args) > 1 else None
@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:
await utils.answer(message, self.strings("no_username"))
return
return await message.edit(self.strings("no_username"))
user_data = await self.github_fetch(f"https://api.github.com/users/{username}")
if "message" in user_data:
await utils.answer(message, self.strings("user_not_found").format(username))
return
data = self.github_api(f"https://api.github.com/users/{username}")
if not data:
return await message.edit(self.strings("user_not_found").format(username))
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 = (
self.strings("profile_header").format(url=user_data["html_url"], username=username)
+ self.strings("profile_text").format(
name=user_data.get("name") or self.strings("not_specified"),
login=username,
bio=user_data.get("bio") or self.strings("no_desc"),
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")
)
@loader.command(doc="Show recent GitHub activity", ru_doc="Последняя активность GitHub")
async def gha(self, message):
"""Show recent GitHub activity"""
username = self.get_username(message)
if not username:
return await message.edit(self.strings("no_username"))
await self.inline.form(
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
events = self.github_api(f"https://api.github.com/users/{username}/events?per_page=5")
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
return await message.edit(self.strings("no_activity").format(username))
lines = [self.strings("activity_header").format(username=username)]
seen_repos = set()
for event in events[:25]:
lines = []
for event in events:
etype = event["type"]
repo = event["repo"]["name"]
if repo in seen_repos and len(lines) > 20:
continue
payload = event.get("payload", {})
if etype == "PushEvent":
branch = payload.get("ref", "refs/heads/main").replace("refs/heads/", "")
commits = payload.get("commits", [])
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)
branch = re.sub(r"refs/heads/", "", payload.get("ref", "main"))
count = len(payload.get("commits", []))
lines.append(self.strings("activity_commit").format(count=count, branch=branch, repo=repo))
elif etype == "CreateEvent":
ref_type = payload.get("ref_type")
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))
lines.append(self.strings("activity_create").format(ref_type=payload.get("ref_type"), repo=repo))
elif etype == "PullRequestEvent":
pr = payload.get("pull_request", {})
number = pr.get("number", "?")
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))
lines.append(self.strings("activity_pr").format(action=payload.get("action"), title=pr.get("title")))
elif etype == "IssuesEvent":
issue = payload.get("issue", {})
number = issue.get("number", "?")
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))
lines.append(self.strings("activity_issue").format(action=payload.get("action"), title=issue.get("title")))
elif etype == "WatchEvent":
lines.append(self.strings("star").format(repo=repo))
lines.append(self.strings("activity_star").format(repo=repo))
elif etype == "ForkEvent":
fork = payload.get("forkee", {}).get("full_name", "unknown")
lines.append(self.strings("fork").format(fork=fork))
lines.append(self.strings("activity_fork").format(fork=payload.get("forkee", {}).get("full_name")))
else:
event_name = etype.replace("Event", "")
lines.append(self.strings("other").format(event=event_name, repo=repo))
lines.append(self.strings("activity_other").format(event=etype, repo=repo))
await call.edit(
text="".join(lines),
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
)
await message.edit(self.strings("activity_header") + "\n".join(lines))
async def _repos(self, call: InlineCall, username: str):
repos = await self.github_fetch(f"https://api.github.com/users/{username}/repos?sort=stars&per_page=10")
if "message" in repos:
await call.edit(self.strings("api_error").format(msg=repos["message"]), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
if not repos:
await call.edit(self.strings("no_repos"), reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]])
return
@loader.command(doc="Show GitHub contribution graph", ru_doc="Показать график контрибов GitHub")
async def ghc(self, message):
"""Show GitHub contribution graph"""
username = self.get_username(message)
if not username:
return await message.edit(self.strings("no_username"))
lines = [self.strings("repos_header").format(username=username)]
for repo in repos[:10]:
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),
))
data = self.github_api(f"https://github-contributions-api.deno.dev/{username}.json")
contribs = data.get("contributions") if data else None
await call.edit(
text="".join(lines),
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)])
if not isinstance(contribs, list):
return await message.edit(self.strings("no_contrib").format(username))
today = datetime.utcnow().date()
weeks_count = 53
days_back = weeks_count * 7 + 7
start = today - timedelta(days=days_back)
start = today - timedelta(days=90)
matrix = [["" for _ in range(13)] for _ in range(7)]
matrix = [["" for _ in range(weeks_count)] for _ in range(7)]
for entry in raw_days:
date_str = entry.get("date")
if not date_str:
continue
for entry in contribs:
try:
date = datetime.strptime(date_str, "%Y-%m-%d").date()
if date < start or date > today:
date = datetime.strptime(entry["date"], "%Y-%m-%d").date()
if not (start <= date <= today):
continue
count = entry.get("contributionCount") or entry.get("count", 0) or 0
if count > 0:
day_idx = (date.weekday() + 1) % 7
week_idx = (date - start).days // 7
if week_idx < weeks_count:
matrix[day_idx][week_idx] = "🟩"
except Exception:
day = (date.weekday() + 1) % 7 # Sunday=0
week = (date - start).days // 7
if entry.get("contributionCount", 0) > 0:
matrix[day][week] = "🟩"
except:
continue
days_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
graph = "\n".join(f"{days_labels[i]} {''.join(matrix[i])}" for i in range(7))
days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
graph = "\n".join(f"{days[i]} {''.join(matrix[i])}" for i in range(7))
await call.edit(
text=self.strings("contrib_header").format(username=username)
+ f"<pre>{graph}</pre>"
+ self.strings("contrib_footer"),
reply_markup=[[{"text": self.strings("btn_back"), "callback": self._profile, "args": (username,)}]]
await message.edit(
self.strings("contrib_header").format(username=username)
+ f"<pre>{graph}</pre>\n"
+ self.strings("contrib_footer")
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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