ruff formatting

This commit is contained in:
Vladless
2025-05-09 01:34:11 +03:00
parent e4a73f9aaa
commit 5ddf7f8242
14 changed files with 133 additions and 117 deletions
+11 -13
View File
@@ -28,7 +28,7 @@ console = Console()
BACK_DIR = os.path.expanduser("~/.solobot_backup")
TEMP_DIR = os.path.expanduser("~/.solobot_tmp")
PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
IS_ROOT_DIR = PROJECT_DIR == '/root'
IS_ROOT_DIR = PROJECT_DIR == "/root"
if IS_ROOT_DIR:
console.print("[bold red]⛔ КРИТИЧЕСКАЯ ОШИБКА:[/bold red]")
@@ -73,12 +73,12 @@ def fix_permissions():
"""Устанавливает корректные права на файлы проекта"""
console.print("[yellow]🔧 Устанавливаю права на файлы...[/yellow]")
try:
user = os.getenv('SUDO_USER') or os.getenv('USER')
user = os.getenv("SUDO_USER") or os.getenv("USER")
if user:
subprocess.run(["sudo", "chown", "-R", f"{user}:{user}", PROJECT_DIR], check=True)
subprocess.run(["sudo", "chmod", "-R", "u=rwX,go=rX", PROJECT_DIR], check=True)
console.print("[green]✅ Права успешно установлены[/green]")
except subprocess.CalledProcessError as e:
console.print(f"[red]❌ Ошибка при установке прав: {e}[/red]")
@@ -195,7 +195,7 @@ def update_from_beta():
if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot с ветки DEV[/yellow]"):
return
console.print("[red]⚠️ ВНИМАНИЕ! Папка бота будет перезаписана![/red]")
if not Confirm.ask("[red]❓ Продолжить обновление?[/red]"):
return
@@ -215,11 +215,11 @@ def update_from_beta():
subprocess.run(["sudo", "rm", "-rf", os.path.join(PROJECT_DIR, "venv")])
clean_project_dir_safe(update_buttons=update_buttons)
exclude_options = "--exclude=img"
if not update_buttons:
exclude_options += " --exclude=handlers/buttons.py"
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
subprocess.run(["rm", "-rf", TEMP_DIR])
@@ -258,8 +258,7 @@ def update_from_release():
console.print(f"[cyan]{idx}.[/cyan] {tag}")
selected = Prompt.ask(
"[bold blue]Выберите номер релиза[/bold blue]",
choices=[str(i) for i in range(1, len(tag_choices) + 1)]
"[bold blue]Выберите номер релиза[/bold blue]", choices=[str(i) for i in range(1, len(tag_choices) + 1)]
)
tag_name = tag_choices[int(selected) - 1]
@@ -277,7 +276,7 @@ def update_from_release():
exclude_options = "--exclude=img"
if not update_buttons:
exclude_options += " --exclude=handlers/buttons.py"
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
subprocess.run(["rm", "-rf", TEMP_DIR])
@@ -291,7 +290,6 @@ def update_from_release():
def show_update_menu():
if IS_ROOT_DIR:
console.print("[red]⛔ Обновление невозможно: бот находится в /root[/red]")
console.print("[yellow]Перенесите бота в отдельную папку и повторите попытку[/yellow]")
@@ -335,9 +333,9 @@ def update_cli_launcher():
try:
url = "https://raw.githubusercontent.com/Vladless/Solo_bot/dev/cli_launcher.py"
response = requests.get(url, timeout=10)
if response.status_code == 200:
with open(os.path.join(PROJECT_DIR, "cli_launcher.py"), 'w', encoding='utf-8') as f:
with open(os.path.join(PROJECT_DIR, "cli_launcher.py"), "w", encoding="utf-8") as f:
f.write(response.text)
console.print("[green]✅ CLI лаунчер успешно обновлён[/green]")
os.chmod(os.path.join(PROJECT_DIR, "cli_launcher.py"), 0o755)
@@ -6,10 +6,10 @@ from asyncpg import Connection
from filters.admin import IsAdminFilter
from logger import logger
from middlewares import maintenance
from ..panel.keyboard import build_admin_back_kb
from .keyboard import AdminPanelCallback, build_management_kb
from middlewares import maintenance
router = Router()
@@ -78,10 +78,9 @@ async def process_new_domain(message: Message, state: FSMContext, session: Conne
@router.callback_query(AdminPanelCallback.filter(F.action == "toggle_maintenance"))
async def toggle_maintenance_mode(callback: CallbackQuery):
maintenance.maintenance_mode = not maintenance.maintenance_mode
new_status = "включён" if maintenance.maintenance_mode else "выключен"
await callback.answer(f"🛠️ Режим обслуживания {new_status}.", show_alert=True)
await callback.message.edit_reply_markup(reply_markup=build_management_kb())
await callback.message.edit_reply_markup(reply_markup=build_management_kb())
+38 -34
View File
@@ -24,22 +24,36 @@ async def generate_captcha(message: Message, state: FSMContext):
all_emojis = [correct_emoji] + wrong_emojis
random.shuffle(all_emojis)
from_user = message.from_user or message.chat
state_data = await state.get_data()
await state.update_data(
correct_emoji=correct_emoji,
message_id=message.message_id,
chat_id=message.chat.id,
original_text=message.text,
user_data={
"tg_id": from_user.id,
"username": getattr(from_user, "username", None),
"first_name": getattr(from_user, "first_name", None),
"last_name": getattr(from_user, "last_name", None),
"language_code": getattr(from_user, "language_code", None),
"is_bot": getattr(from_user, "is_bot", False),
}
)
if "user_data" not in state_data:
from_user = message.from_user
if not from_user:
logger.warning("[CAPTCHA] ❗ from_user отсутствует — невозможно сохранить user_data")
return None
await state.update_data(
user_data={
"tg_id": from_user.id,
"username": getattr(from_user, "username", None),
"first_name": getattr(from_user, "first_name", None),
"last_name": getattr(from_user, "last_name", None),
"language_code": getattr(from_user, "language_code", None),
"is_bot": getattr(from_user, "is_bot", False),
}
)
update_data = {
"correct_emoji": correct_emoji,
"message_id": message.message_id,
"chat_id": message.chat.id,
}
state_data = await state.get_data()
if "original_text" not in state_data:
update_data["original_text"] = message.text
await state.update_data(**update_data)
builder = InlineKeyboardBuilder()
for emoji in all_emojis:
@@ -59,38 +73,28 @@ async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any
selected_emoji = callback.data.split("captcha_")[1]
state_data = await state.get_data()
correct_emoji = state_data.get("correct_emoji")
message_id = state_data.get("message_id")
chat_id = state_data.get("chat_id")
original_text = state_data.get("original_text")
user_data = state_data.get("user_data")
if not message_id or not chat_id:
target_message = callback.message
else:
try:
target_message = await callback.bot.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=callback.message.text,
)
except Exception:
target_message = callback.message
target_message = callback.message
if selected_emoji == correct_emoji:
logger.info(f"Пользователь {callback.from_user.id} успешно прошел капчу")
logger.debug(f"[CAPTCHA] user_data передано в process_start_logic: {user_data}")
await process_start_logic(
message=target_message,
state=state,
session=session,
admin=admin,
text_to_process=original_text,
user_data=user_data
user_data=user_data,
)
else:
logger.warning(f"Пользователь {callback.from_user.id} неверно ответил на капчу")
captcha = await generate_captcha(target_message, state)
await edit_or_send_message(
target_message=target_message,
text=captcha["text"],
reply_markup=captcha["markup"],
)
if captcha:
await edit_or_send_message(
target_message=target_message,
text=captcha["text"],
reply_markup=captcha["markup"],
)
+8 -3
View File
@@ -3,8 +3,8 @@ import uuid
from datetime import datetime
from typing import Any
import asyncpg
import asyncpg
import pytz
from aiogram import F, Router
@@ -14,10 +14,10 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from config import (
DATABASE_URL,
ADMIN_PASSWORD,
ADMIN_USERNAME,
CONNECT_PHONE_BUTTON,
DATABASE_URL,
PUBLIC_LINK,
REMNAWAVE_LOGIN,
REMNAWAVE_PASSWORD,
@@ -39,7 +39,12 @@ from handlers.texts import (
SELECT_COUNTRY_MSG,
key_message_success,
)
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster, is_full_remnawave_cluster
from handlers.utils import (
edit_or_send_message,
generate_random_email,
get_least_loaded_cluster,
is_full_remnawave_cluster,
)
from logger import logger
from panels.remnawave import RemnawaveAPI
from panels.three_xui import delete_client, get_xui_instance
+3 -1
View File
@@ -232,7 +232,9 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
f"Осталось: <b>{format_days(days)}</b>, <b>{format_hours(hours)}</b>, <b>{format_minutes(minutes)}</b>"
)
formatted_expiry_date = f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года"
formatted_expiry_date = (
f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года"
)
response_message = key_message(
final_link,
formatted_expiry_date,
+7 -3
View File
@@ -12,8 +12,8 @@ from aiohttp import web
from config import (
DATABASE_URL,
RANDOM_SUBSCRIPTIONS,
PROJECT_NAME,
RANDOM_SUBSCRIPTIONS,
SUPERNODE,
SUPPORT_CHAT_URL,
USERNAME_BOT,
@@ -41,7 +41,9 @@ async def fetch_url_content(url: str, identifier: str) -> tuple[list[str], dict[
return [], {}
async def combine_unique_lines(urls: list[str], identifier: str, query_string: str) -> tuple[list[str], list[dict[str, str]]]:
async def combine_unique_lines(
urls: list[str], identifier: str, query_string: str
) -> tuple[list[str], list[dict[str, str]]]:
if SUPERNODE:
logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для идентификатора: {identifier}")
if not urls:
@@ -82,7 +84,9 @@ async def get_subscription_urls(server_id: str, email: str, conn, include_remnaw
return urls
def calculate_traffic(cleaned_subscriptions: list[str], expiry_time_ms: int | None, headers_list: list[dict[str, str]]) -> str:
def calculate_traffic(
cleaned_subscriptions: list[str], expiry_time_ms: int | None, headers_list: list[dict[str, str]]
) -> str:
logger.debug(f"Calculating traffic with subscriptions: {cleaned_subscriptions}, headers: {headers_list}")
expire_timestamp = int(expiry_time_ms / 1000) if expiry_time_ms else 0
@@ -1,9 +1,12 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
from aiogram import Bot, Router
import pytz
from aiogram import Bot, Router
from config import (
DATABASE_URL,
NOTIFICATION_TIME,
@@ -176,7 +179,9 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
if NOTIFY_RENEW:
try:
await process_auto_renew_or_notify(bot, conn, key, notification_id, 1, "notify_24h.jpg", notification_text)
await process_auto_renew_or_notify(
bot, conn, key, notification_id, 1, "notify_24h.jpg", notification_text
)
except Exception as e:
logger.error(f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {e}")
continue
@@ -193,14 +198,16 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
if messages:
results = await send_messages_with_limit(bot, messages, conn=conn)
sent_count = 0
for msg, result in zip(messages, results):
for msg, result in zip(messages, results, strict=False):
tg_id = msg["tg_id"]
if result:
await add_notification(tg_id, msg["notification_id"], session=conn)
sent_count += 1
logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
else:
logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
logger.warning(
f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}."
)
logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 24 часа.")
logger.info("Обработка всех уведомлений за 24 часа завершена.")
@@ -273,14 +280,16 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int,
if messages:
results = await send_messages_with_limit(bot, messages, conn=conn)
sent_count = 0
for msg, result in zip(messages, results):
for msg, result in zip(messages, results, strict=False):
tg_id = msg["tg_id"]
if result:
await add_notification(tg_id, msg["notification_id"], session=conn)
sent_count += 1
logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
else:
logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
logger.warning(
f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}."
)
logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 10 часов.")
logger.info("Обработка всех уведомлений за 10 часов завершена.")
@@ -395,7 +404,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
if messages:
results = await send_messages_with_limit(bot, messages, conn=conn)
sent_count = 0
for msg, result in zip(messages, results):
for msg, result in zip(messages, results, strict=False):
tg_id = msg["tg_id"]
email = msg["email"]
if result:
@@ -403,7 +412,9 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
sent_count += 1
logger.info(f"📢 Отправлено уведомление об истекшем ключе для подписки {email} пользователю {tg_id}.")
else:
logger.warning(f"📢 Не удалось отправить уведомление об истекшем ключе для подписки {email} пользователю {tg_id}.")
logger.warning(
f"📢 Не удалось отправить уведомление об истекшем ключе для подписки {email} пользователю {tg_id}."
)
logger.info(f"Отправлено {sent_count} уведомлений об истекших ключах.")
logger.info("Обработка истекших ключей завершена.")
+5 -9
View File
@@ -3,6 +3,7 @@ import os
import aiofiles
import asyncpg
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup
@@ -16,7 +17,7 @@ async def send_messages_with_limit(
messages: list[dict],
conn: asyncpg.Connection = None,
source_file: str = None,
messages_per_second: int = 25
messages_per_second: int = 25,
):
"""
Отправляет сообщения с ограничением по количеству сообщений в секунду.
@@ -28,16 +29,10 @@ async def send_messages_with_limit(
batch = messages[i : i + batch_size]
tasks = []
for msg in batch:
tasks.append(send_notification(
bot,
msg["tg_id"],
msg.get("photo"),
msg["text"],
msg.get("keyboard")
))
tasks.append(send_notification(bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard")))
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
processed_results = []
for msg, result in zip(batch, batch_results):
for msg, result in zip(batch, batch_results, strict=False):
tg_id = msg["tg_id"]
if isinstance(result, bool) and result:
processed_results.append(True)
@@ -94,6 +89,7 @@ def rate_limited_send(func):
tg_id = kwargs.get("tg_id") or args[1]
logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}")
return False
return wrapper
@@ -1,8 +1,10 @@
import asyncio
from datetime import datetime, timedelta
import asyncpg
import pytz
from aiogram import Bot, Router, types
from aiogram.utils.keyboard import InlineKeyboardBuilder
@@ -23,6 +25,7 @@ from handlers.texts import (
)
from handlers.utils import format_days
from logger import logger
from .notify_utils import send_messages_with_limit, send_notification
@@ -76,14 +79,10 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
})
if messages:
results = await send_messages_with_limit(
bot,
messages,
conn=conn,
source_file="special_notifications",
messages_per_second=25
bot, messages, conn=conn, source_file="special_notifications", messages_per_second=25
)
sent_count = 0
for msg, result in zip(messages, results):
for msg, result in zip(messages, results, strict=False):
tg_id = msg["tg_id"]
if result:
await add_notification(tg_id, msg["notification_id"], session=conn)
@@ -148,9 +147,7 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
)
try:
await conn.execute(
"UPDATE keys SET notified = TRUE WHERE tg_id = $1 AND client_id = $2", tg_id, client_id
)
await conn.execute("UPDATE keys SET notified = TRUE WHERE tg_id = $1 AND client_id = $2", tg_id, client_id)
except Exception as e:
logger.error(f"Ошибка обновления notified для пользователя {tg_id} (client_id: {client_id}): {e}")
continue
@@ -171,14 +168,10 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
if messages:
results = await send_messages_with_limit(
bot,
messages,
conn=conn,
source_file="special_notifications",
messages_per_second=25
bot, messages, conn=conn, source_file="special_notifications", messages_per_second=25
)
sent_count = 0
for msg, result in zip(messages, results):
for msg, result in zip(messages, results, strict=False):
tg_id = msg["tg_id"]
if result:
sent_count += 1
+1 -1
View File
@@ -142,7 +142,7 @@ async def process_start_logic(
session: Any,
admin: bool,
text_to_process: str = None,
user_data: dict | None = None
user_data: dict | None = None,
):
text = text_to_process or message.text or message.caption
user_data = user_data or {
+18 -15
View File
@@ -4,13 +4,14 @@ import re
import secrets
import string
from datetime import datetime
import aiofiles
import aiohttp
import asyncpg
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
from datetime import datetime
from bot import bot
from config import ADMIN_ID, DATABASE_URL
from database import get_all_keys, get_servers
@@ -146,12 +147,14 @@ def format_days(days: int) -> str:
return "0 дней"
return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}"
def format_hours(hours: int) -> str:
"""Форматирует количество часов с правильным склонением"""
if hours <= 0:
return "0 часов"
return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}"
def format_minutes(minutes: int) -> str:
"""Форматирует количество минут с правильным склонением"""
if minutes <= 0:
@@ -272,28 +275,28 @@ def sanitize_key_name(key_name: str) -> str:
RUSSIAN_MONTHS = {
'January': 'Января',
'February': 'Февраля',
'March': 'Марта',
'April': 'Апреля',
'May': 'Мая',
'June': 'Июня',
'July': 'Июля',
'August': 'Августа',
'September': 'Сентября',
'October': 'Октября',
'November': 'Ноября',
'December': 'Декабря'
"January": "Января",
"February": "Февраля",
"March": "Марта",
"April": "Апреля",
"May": "Мая",
"June": "Июня",
"July": "Июля",
"August": "Августа",
"September": "Сентября",
"October": "Октября",
"November": "Ноября",
"December": "Декабря",
}
def get_russian_month(date: datetime) -> str:
"""
Преобразует английское название месяца в русское.
Args:
date: Объект datetime, из которого извлекается месяц.
Returns:
Название месяца на русском языке.
"""
+3 -5
View File
@@ -5,11 +5,10 @@ from aiogram.dispatcher.middlewares.base import BaseMiddleware
from .admin import AdminMiddleware
from .loggings import LoggingMiddleware
from .maintenance import MaintenanceModeMiddleware
from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware
from .user import UserMiddleware
from .maintenance import MaintenanceModeMiddleware
def register_middleware(
@@ -17,13 +16,12 @@ def register_middleware(
middlewares: Iterable[BaseMiddleware | type[BaseMiddleware]] | None = None,
exclude: Iterable[str] | None = None,
) -> None:
"""Регистрирует middleware в диспетчере.
"""
"""Регистрирует middleware в диспетчере."""
if middlewares is None:
available_middlewares = {
"admin": AdminMiddleware(),
"session": SessionMiddleware(),
"maintenance": MaintenanceModeMiddleware(),
"maintenance": MaintenanceModeMiddleware(),
"logging": LoggingMiddleware(),
"throttling": ThrottlingMiddleware(),
"user": UserMiddleware(),
+5 -2
View File
@@ -1,9 +1,12 @@
from aiogram import BaseMiddleware
from aiogram.types import Message, CallbackQuery
from aiogram.types import CallbackQuery, Message
from config import ADMIN_ID
maintenance_mode = False
class MaintenanceModeMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
if maintenance_mode:
@@ -16,5 +19,5 @@ class MaintenanceModeMiddleware(BaseMiddleware):
if user_id and user_id not in ADMIN_ID:
await event.answer("⚙️ Бот временно недоступен. Ведутся технические работы.")
return
return await handler(event, data)
+4 -4
View File
@@ -1,6 +1,6 @@
import csv
from datetime import datetime
from datetime import datetime
from io import StringIO
from typing import Any
@@ -185,9 +185,9 @@ async def export_keys_csv(session) -> BufferedInputFile:
buffer.write("tg_id,client_id,email,created_at,expiry_time,key,server_id,is_frozen,alias\n")
for row in keys:
created_at = datetime.utcfromtimestamp(row['created_at'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
expiry_time = datetime.utcfromtimestamp(row['expiry_time'] / 1000).strftime("%Y-%m-%d %H:%M:%S")
created_at = datetime.utcfromtimestamp(row["created_at"] / 1000).strftime("%Y-%m-%d %H:%M:%S")
expiry_time = datetime.utcfromtimestamp(row["expiry_time"] / 1000).strftime("%Y-%m-%d %H:%M:%S")
buffer.write(
f"{row['tg_id']},{row['client_id']},{row['email']},"
f"{created_at},{expiry_time},{row['key']},"