custom emoji in inline buttons/ CLI 0.3.9/ updated dependencies/ other minor changes
This commit is contained in:
@@ -20,4 +20,7 @@ RUN rm -rf /app/venv \
|
||||
&& /app/venv/bin/pip install --upgrade pip \
|
||||
&& /app/venv/bin/pip install -r requirements.txt
|
||||
|
||||
RUN adduser --disabled-password --gecos "" appuser && chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
CMD ["/app/venv/bin/python", "main.py"]
|
||||
|
||||
@@ -5,10 +5,12 @@ from aiogram.fsm.storage.memory import MemoryStorage
|
||||
|
||||
from config import API_TOKEN
|
||||
from filters.private import IsPrivateFilter
|
||||
from utils.button_icons import apply_button_icons_patch, set_button_icon_config
|
||||
from utils.custom_emojis import initialize_custom_emojis
|
||||
from utils.errors import setup_error_handlers
|
||||
from utils.modules_loader import load_modules_from_folder, modules_hub
|
||||
|
||||
apply_button_icons_patch()
|
||||
|
||||
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
storage = MemoryStorage()
|
||||
@@ -18,6 +20,10 @@ dp.include_router(modules_hub)
|
||||
|
||||
load_modules_from_folder()
|
||||
|
||||
from handlers.buttons import BUTTON_ICON_CONFIG
|
||||
|
||||
set_button_icon_config(BUTTON_ICON_CONFIG)
|
||||
|
||||
dp.message.filter(IsPrivateFilter())
|
||||
dp.callback_query.filter(IsPrivateFilter())
|
||||
|
||||
|
||||
+15
-11
@@ -210,7 +210,10 @@ def restore_from_backup():
|
||||
install_rsync_if_needed()
|
||||
|
||||
console.print("[yellow]Копирую файлы из бэкапа в проект...[/yellow]")
|
||||
rc = subprocess.run(f"rsync -a --delete {sel_path}/ {PROJECT_DIR}/", shell=True).returncode
|
||||
rc = subprocess.run(
|
||||
["rsync", "-a", "--delete", f"{sel_path}/", f"{PROJECT_DIR}/"],
|
||||
check=False,
|
||||
).returncode
|
||||
if rc != 0:
|
||||
console.print("[red]❌ Ошибка rsync при восстановлении[/red]")
|
||||
return
|
||||
@@ -239,7 +242,7 @@ def auto_update_cli():
|
||||
console.print("[green]Доступна новая версия CLI. Обновляю...[/green]")
|
||||
with open(current_path, "w", encoding="utf-8") as f:
|
||||
f.write(latest_text)
|
||||
os.chmod(current_path, 0o755)
|
||||
os.chmod(current_path, 0o644)
|
||||
console.print("[green]CLI обновлён. Перезапуск...[/green]")
|
||||
os.execv(sys.executable, [sys.executable, current_path])
|
||||
else:
|
||||
@@ -370,13 +373,13 @@ def install_dependencies():
|
||||
shutil.rmtree("venv")
|
||||
console.print("[yellow]Удалён старый venv[/yellow]")
|
||||
|
||||
subprocess.run(f"{python312_path} -m venv venv", shell=True, check=True)
|
||||
subprocess.run([python312_path, "-m", "venv", "venv"], check=True)
|
||||
|
||||
progress.update(task_id, description="Установка зависимостей...")
|
||||
subprocess.run(
|
||||
"bash -c 'source venv/bin/activate && pip install -r requirements.txt'",
|
||||
shell=True,
|
||||
[os.path.join("venv", "bin", "pip"), "install", "-r", "requirements.txt"],
|
||||
check=True,
|
||||
cwd=PROJECT_DIR,
|
||||
)
|
||||
|
||||
progress.update(task_id, description="Установка завершена")
|
||||
@@ -390,7 +393,7 @@ def restart_service():
|
||||
if is_service_exists(SERVICE_NAME):
|
||||
console.print("[blue]🚀 Перезапуск службы...[/blue]")
|
||||
with console.status("[bold yellow]Перезапуск...[/bold yellow]"):
|
||||
subprocess.run(f"sudo systemctl restart {SERVICE_NAME}", shell=True)
|
||||
subprocess.run(["sudo", "systemctl", "restart", SERVICE_NAME])
|
||||
else:
|
||||
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
|
||||
|
||||
@@ -482,7 +485,8 @@ def update_from_beta():
|
||||
exclude_options += "--exclude=handlers/buttons.py "
|
||||
exclude_options += "--exclude=modules "
|
||||
|
||||
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
|
||||
rsync_cmd = ["rsync", "-a"] + [x for x in exclude_options.split() if x] + [f"{TEMP_DIR}/", f"{PROJECT_DIR}/"]
|
||||
subprocess.run(rsync_cmd)
|
||||
|
||||
modules_path = os.path.join(PROJECT_DIR, "modules")
|
||||
if not os.path.exists(modules_path):
|
||||
@@ -544,8 +548,7 @@ def update_from_release():
|
||||
console.print(f"[cyan]Клонируем релиз {tag_name} во временную папку...[/cyan]")
|
||||
subprocess.run(["rm", "-rf", TEMP_DIR])
|
||||
subprocess.run(
|
||||
f"git clone --branch {tag_name} {GITHUB_REPO} {TEMP_DIR}",
|
||||
shell=True,
|
||||
["git", "clone", "--branch", tag_name, GITHUB_REPO, TEMP_DIR],
|
||||
check=True,
|
||||
)
|
||||
|
||||
@@ -560,7 +563,8 @@ def update_from_release():
|
||||
exclude_options += "--exclude=handlers/buttons.py "
|
||||
exclude_options += "--exclude=modules "
|
||||
|
||||
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
|
||||
rsync_cmd = ["rsync", "-a"] + exclude_options.split() + [f"{TEMP_DIR}/", f"{PROJECT_DIR}/"]
|
||||
subprocess.run(rsync_cmd)
|
||||
|
||||
modules_path = os.path.join(PROJECT_DIR, "modules")
|
||||
if not os.path.exists(modules_path):
|
||||
@@ -608,7 +612,7 @@ def show_update_menu():
|
||||
|
||||
|
||||
def show_menu():
|
||||
table = Table(title="Solobot CLI v0.3.8", title_style="bold magenta", header_style="bold blue")
|
||||
table = Table(title="Solobot CLI v0.3.9", title_style="bold magenta", header_style="bold blue")
|
||||
table.add_column("№", justify="center", style="cyan", no_wrap=True)
|
||||
table.add_column("Операция", style="white")
|
||||
table.add_row("1", "Запустить бота (systemd)")
|
||||
|
||||
@@ -161,7 +161,10 @@ async def get_total_referral_bonus(session: AsyncSession, referrer_tg_id: int, m
|
||||
"""
|
||||
)
|
||||
|
||||
result = await session.execute(text(bonus_query), {"tg_id": referrer_tg_id, "max_levels": max_levels})
|
||||
result = await session.execute(
|
||||
text(bonus_query), # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
|
||||
{"tg_id": referrer_tg_id, "max_levels": max_levels},
|
||||
)
|
||||
total_bonus_raw = result.scalar()
|
||||
total_bonus = round(float(total_bonus_raw or 0), 2)
|
||||
|
||||
@@ -189,7 +192,10 @@ async def get_referrals_by_level(session: AsyncSession, referrer_tg_id: int, max
|
||||
GROUP BY level
|
||||
ORDER BY level
|
||||
"""
|
||||
result = await session.execute(text(query), {"referrer_tg_id": referrer_tg_id, "max_levels": max_levels})
|
||||
result = await session.execute(
|
||||
text(query), # nosemgrep: python.sqlalchemy.security.audit.avoid-sqlalchemy-text.avoid-sqlalchemy-text
|
||||
{"referrer_tg_id": referrer_tg_id, "max_levels": max_levels},
|
||||
)
|
||||
return {
|
||||
row["level"]: {
|
||||
"total": row["level_count"],
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
BUTTON_ICON_CONFIG: dict[str, dict[str, str]] = {
|
||||
"partner": {"icon_custom_emoji_id": "5310169226856644648", "style": "primary"},
|
||||
# Примеры по callback_data:
|
||||
# "profile": {"icon_custom_emoji_id": "5310169226856644648", "style": "primary"}, # синяя
|
||||
# "view_keys": {"icon_custom_emoji_id": "5310169226856644648", "style": "success"}, # зеленая
|
||||
# "pay": {"style": "primary"},
|
||||
# "cancel_broadcast": {"style": "danger"}, # красная
|
||||
}
|
||||
|
||||
# Общие кнопки
|
||||
BACK = "⬅️ Назад"
|
||||
APPLY = "✅ Подтвердить"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
from aiogram import Dispatcher
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
from aiogram import BaseMiddleware, Dispatcher
|
||||
|
||||
from config import CHANNEL_REQUIRED, DISABLE_DIRECT_START
|
||||
from middlewares.ban_checker import BanCheckerMiddleware
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
from aiogram import BaseMiddleware
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
+8
-8
@@ -3,15 +3,15 @@ name = "Solo_bot"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"aiofiles==24.1.0",
|
||||
"aiogram==3.13.1",
|
||||
"aiohappyeyeballs==2.4.3",
|
||||
"aiohttp==3.10.10",
|
||||
"aiosignal==1.3.1",
|
||||
"aiogram>=3.24.0",
|
||||
"aiohappyeyeballs>=2.5.0",
|
||||
"aiohttp==3.13.3",
|
||||
"aiosignal>=1.4.0",
|
||||
"annotated-types==0.7.0",
|
||||
"async-timeout==4.0.3",
|
||||
"asyncpg==0.30.0",
|
||||
"attrs==24.2.0",
|
||||
"certifi",
|
||||
"certifi>=2023.5.7,<2024.0.0", # ограничение из aiocryptopay; CVE-2024-39689 исправлен в 2024.7.4
|
||||
"charset-normalizer==3.4.0",
|
||||
"deprecated==1.2.14",
|
||||
"distro==1.9.0",
|
||||
@@ -23,11 +23,11 @@ dependencies = [
|
||||
"propcache==0.2.0",
|
||||
"pydantic",
|
||||
"pydantic-core",
|
||||
"requests==2.32.3",
|
||||
"requests==2.32.4",
|
||||
"typing-extensions==4.12.2",
|
||||
"urllib3==2.2.3",
|
||||
"urllib3==2.6.3",
|
||||
"wrapt==1.16.0",
|
||||
"yarl==1.15.5",
|
||||
"yarl>=1.17.0,<2.0",
|
||||
"yookassa==3.3.0",
|
||||
"loguru",
|
||||
"aiocryptopay",
|
||||
|
||||
+12
-12
@@ -1,9 +1,9 @@
|
||||
aiocryptopay==0.4.7
|
||||
aiofiles==24.1.0
|
||||
aiogram==3.13.1
|
||||
aiohappyeyeballs==2.4.3
|
||||
aiohttp==3.10.10
|
||||
aiosignal==1.3.1
|
||||
aiogram==3.24.0
|
||||
aiohappyeyeballs==2.5.0
|
||||
aiohttp==3.13.3
|
||||
aiosignal>=1.4.0
|
||||
alembic==1.16.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
@@ -13,18 +13,18 @@ asyncpg==0.30.0
|
||||
attrs==24.2.0
|
||||
babel==2.17.0
|
||||
cachetools==5.5.1
|
||||
certifi==2023.11.17
|
||||
certifi==2023.11.17 # aiocryptopay требует certifi<2024; обновить после выхода совместимой версии aiocryptopay
|
||||
cffi==1.17.1
|
||||
charset-normalizer==3.4.0
|
||||
click==8.2.2
|
||||
cryptography==45.0.5
|
||||
Deprecated==1.2.14
|
||||
distro==1.9.0
|
||||
fastapi==0.116.1
|
||||
fastapi==0.128.7
|
||||
frozenlist==1.4.1
|
||||
greenlet==3.1.1
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.27.2
|
||||
idna==3.10
|
||||
loguru==0.7.3
|
||||
@@ -48,19 +48,19 @@ Pygments==2.19.2
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2025.1
|
||||
qrcode==8.2
|
||||
requests==2.32.3
|
||||
requests==2.32.4
|
||||
rich==14.1.0
|
||||
robokassa==0.3.2
|
||||
ruff==0.9.5
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
SQLAlchemy==2.0.38
|
||||
starlette==0.47.2
|
||||
starlette==0.49.1
|
||||
StrEnum==0.4.15
|
||||
typing_extensions==4.12.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.2.3
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.35.0
|
||||
wrapt==1.16.0
|
||||
yarl==1.15.5
|
||||
yarl>=1.17.0,<2.0
|
||||
yookassa==3.9.0
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import aiogram.types
|
||||
|
||||
_OriginalInlineKeyboardButton = aiogram.types.InlineKeyboardButton
|
||||
|
||||
_button_icon_config: dict[str, dict[str, str]] = {}
|
||||
|
||||
|
||||
def apply_button_icons_patch(config: dict[str, dict[str, str]] | None = None) -> None:
|
||||
"""
|
||||
Патчит InlineKeyboardButton, добавляя поддержку глобального конфига для иконок и стилей кнопок по callback_data или url.
|
||||
"""
|
||||
if config is not None:
|
||||
_button_icon_config.clear()
|
||||
_button_icon_config.update(config)
|
||||
|
||||
class _PatchedInlineKeyboardButton(_OriginalInlineKeyboardButton):
|
||||
def __init__(self, **kwargs: object):
|
||||
key = kwargs.get("callback_data") or kwargs.get("url")
|
||||
if key is not None and isinstance(key, str) and key in _button_icon_config:
|
||||
kwargs = {**kwargs, **_button_icon_config[key]}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
aiogram.types.InlineKeyboardButton = _PatchedInlineKeyboardButton
|
||||
|
||||
|
||||
def set_button_icon_config(config: dict[str, dict[str, str]]) -> None:
|
||||
"""Подставить конфиг кнопок (вызвать после загрузки handlers, из handlers.buttons.BUTTON_ICON_CONFIG)."""
|
||||
_button_icon_config.clear()
|
||||
_button_icon_config.update(config)
|
||||
|
||||
|
||||
def inline_button(
|
||||
text: str,
|
||||
callback_data: str | None = None,
|
||||
url: str | None = None,
|
||||
web_app: object | None = None,
|
||||
*,
|
||||
icon_custom_emoji_id: str | None = None,
|
||||
style: str | None = None,
|
||||
) -> _OriginalInlineKeyboardButton:
|
||||
"""Собирает InlineKeyboardButton с опциональной иконкой (custom emoji) и стилем."""
|
||||
kwargs: dict = {"text": text}
|
||||
if callback_data is not None:
|
||||
kwargs["callback_data"] = callback_data
|
||||
if url is not None:
|
||||
kwargs["url"] = url
|
||||
if web_app is not None:
|
||||
kwargs["web_app"] = web_app
|
||||
if icon_custom_emoji_id is not None:
|
||||
kwargs["icon_custom_emoji_id"] = icon_custom_emoji_id
|
||||
if style is not None:
|
||||
kwargs["style"] = style
|
||||
return aiogram.types.InlineKeyboardButton(**kwargs)
|
||||
@@ -12,12 +12,18 @@ from .modules_manager import manager
|
||||
|
||||
modules_hub = Router(name="modules_hub")
|
||||
|
||||
def _is_safe_module_name(name: str) -> bool:
|
||||
return bool(name and name.isidentifier() and "." not in name and "/" not in name and "\\" not in name)
|
||||
|
||||
|
||||
def load_modules_from_folder(folder: str = "modules") -> list[Router]:
|
||||
routers = []
|
||||
base_path = Path(folder)
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not _is_safe_module_name(name):
|
||||
logger.warning(f"[Modules] Пропуск недопустимого имени модуля: {name!r}")
|
||||
continue
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск автозапуска модуля '{name}' (отключён).")
|
||||
continue
|
||||
@@ -43,6 +49,8 @@ def load_module_webhooks(folder: str = "modules") -> list[dict]:
|
||||
base_path = Path(folder)
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not _is_safe_module_name(name):
|
||||
continue
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск вебхуков модуля '{name}' (отключён).")
|
||||
continue
|
||||
@@ -65,6 +73,8 @@ def load_module_fast_flow_handlers(folder: str = "modules") -> dict:
|
||||
base_path = Path(folder)
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not _is_safe_module_name(name):
|
||||
continue
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск fast-flow модуля '{name}' (отключён).")
|
||||
continue
|
||||
|
||||
@@ -57,7 +57,12 @@ class ModulesManager:
|
||||
rec.enabled = True
|
||||
self.registry[name] = rec
|
||||
|
||||
def _is_safe_module_name(self, name: str) -> bool:
|
||||
return bool(name and name.isidentifier() and "." not in name and "/" not in name and "\\" not in name)
|
||||
|
||||
async def start(self, name: str) -> None:
|
||||
if not self._is_safe_module_name(name):
|
||||
raise ValueError(f"[Modules] Недопустимое имя модуля: {name!r}")
|
||||
rec = self.registry.get(name) or ModuleRecord(name, self.pkg(name))
|
||||
if rec.enabled:
|
||||
logger.info(f"[Modules] {name} уже активен.")
|
||||
|
||||
Reference in New Issue
Block a user