chore: run lint and prettifier

This commit is contained in:
3252a8
2026-05-12 21:54:12 +03:00
parent f31540afdb
commit 11187487b4
174 changed files with 12383 additions and 6688 deletions
+156 -69
View File
@@ -1,13 +1,15 @@
# Bot utilities package
from dataclasses import dataclass
from typing import Optional, Dict, Any
from typing import Any, Dict, Optional
from aiogram import types
@dataclass
class MessageContent:
"""Класс для хранения информации о контенте сообщения"""
content_type: str
file_id: Optional[str] = None
text: Optional[str] = None
@@ -15,15 +17,121 @@ class MessageContent:
# Словари поддерживаемых параметров для каждого типа сообщения
SUPPORTED_PARAMS = {
"text": {"parse_mode", "entities", "disable_web_page_preview", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"photo": {"caption", "parse_mode", "caption_entities", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"video": {"duration", "width", "height", "thumbnail", "caption", "parse_mode", "caption_entities", "supports_streaming", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"animation": {"duration", "width", "height", "thumbnail", "caption", "parse_mode", "caption_entities", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"document": {"thumbnail", "caption", "parse_mode", "caption_entities", "disable_content_type_detection", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"audio": {"caption", "parse_mode", "caption_entities", "duration", "performer", "title", "thumbnail", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"voice": {"caption", "parse_mode", "caption_entities", "duration", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"sticker": {"disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"video_note": {"duration", "length", "thumbnail", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"text": {
"parse_mode",
"entities",
"disable_web_page_preview",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
"photo": {
"caption",
"parse_mode",
"caption_entities",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
"has_spoiler",
},
"video": {
"duration",
"width",
"height",
"thumbnail",
"caption",
"parse_mode",
"caption_entities",
"supports_streaming",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
"has_spoiler",
},
"animation": {
"duration",
"width",
"height",
"thumbnail",
"caption",
"parse_mode",
"caption_entities",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
"has_spoiler",
},
"document": {
"thumbnail",
"caption",
"parse_mode",
"caption_entities",
"disable_content_type_detection",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
"audio": {
"caption",
"parse_mode",
"caption_entities",
"duration",
"performer",
"title",
"thumbnail",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
"voice": {
"caption",
"parse_mode",
"caption_entities",
"duration",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
"sticker": {
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
"video_note": {
"duration",
"length",
"thumbnail",
"disable_notification",
"protect_content",
"reply_markup",
"reply_to_message_id",
"allow_sending_without_reply",
"message_thread_id",
},
}
@@ -39,7 +147,7 @@ def get_message_content(message: types.Message) -> MessageContent:
Использует match/case вместо длинных if-elif цепочек.
"""
text = (message.text or message.caption or "").strip()
# Проверяем наличие медиа-контента
media_content = None
if message.photo:
@@ -58,7 +166,7 @@ def get_message_content(message: types.Message) -> MessageContent:
media_content = ("sticker", message.sticker.file_id)
elif message.video_note:
media_content = ("video_note", message.video_note.file_id)
# Используем match/case для определения типа контента
match media_content:
case (content_type, file_id):
@@ -77,79 +185,69 @@ async def send_message_by_type(bot, chat_id: int, content: MessageContent, **kwa
"""
# Фильтруем kwargs для данного типа сообщения
filtered_kwargs = filter_kwargs(content.content_type, kwargs)
match content.content_type:
case "text":
await bot.send_message(
chat_id=chat_id,
text=content.text,
**filtered_kwargs
)
await bot.send_message(chat_id=chat_id, text=content.text, **filtered_kwargs)
case "photo":
await bot.send_photo(
chat_id=chat_id,
photo=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "video":
await bot.send_video(
chat_id=chat_id,
video=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "animation":
await bot.send_animation(
chat_id=chat_id,
animation=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "document":
await bot.send_document(
chat_id=chat_id,
document=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "audio":
await bot.send_audio(
chat_id=chat_id,
audio=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "voice":
await bot.send_voice(
chat_id=chat_id,
voice=content.file_id,
caption=content.text or None,
**filtered_kwargs
**filtered_kwargs,
)
case "sticker":
await bot.send_sticker(
chat_id=chat_id,
sticker=content.file_id,
**filtered_kwargs
)
await bot.send_sticker(chat_id=chat_id, sticker=content.file_id, **filtered_kwargs)
case "video_note":
await bot.send_video_note(
chat_id=chat_id,
video_note=content.file_id,
**filtered_kwargs
chat_id=chat_id, video_note=content.file_id, **filtered_kwargs
)
case _:
# Fallback для неизвестных типов - отправляем как текст
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id=chat_id,
text=content.text or "Unknown content type",
**text_kwargs
chat_id=chat_id, text=content.text or "Unknown content type", **text_kwargs
)
async def send_message_via_queue(queue_manager, uid: int, content: MessageContent, **kwargs) -> None:
async def send_message_via_queue(
queue_manager, uid: int, content: MessageContent, **kwargs
) -> None:
"""
Отправляет сообщение через очередь в зависимости от типа контента.
Использует match/case вместо длинных if-elif цепочек.
@@ -157,12 +255,10 @@ async def send_message_via_queue(queue_manager, uid: int, content: MessageConten
"""
# Фильтруем kwargs для данного типа сообщения
filtered_kwargs = filter_kwargs(content.content_type, kwargs)
match content.content_type:
case "text":
await queue_manager.send_message(
chat_id=uid, text=content.text, **filtered_kwargs
)
await queue_manager.send_message(chat_id=uid, text=content.text, **filtered_kwargs)
case "photo":
await queue_manager.send_photo(
chat_id=uid, photo=content.file_id, caption=content.text or None, **filtered_kwargs
@@ -173,11 +269,17 @@ async def send_message_via_queue(queue_manager, uid: int, content: MessageConten
)
case "animation":
await queue_manager.send_animation(
chat_id=uid, animation=content.file_id, caption=content.text or None, **filtered_kwargs
chat_id=uid,
animation=content.file_id,
caption=content.text or None,
**filtered_kwargs,
)
case "document":
await queue_manager.send_document(
chat_id=uid, document=content.file_id, caption=content.text or None, **filtered_kwargs
chat_id=uid,
document=content.file_id,
caption=content.text or None,
**filtered_kwargs,
)
case "audio":
await queue_manager.send_audio(
@@ -203,7 +305,9 @@ async def send_message_via_queue(queue_manager, uid: int, content: MessageConten
)
async def send_direct_message(bot, chat_id: int, content: MessageContent, extra_text: str = "", **kwargs) -> None:
async def send_direct_message(
bot, chat_id: int, content: MessageContent, extra_text: str = "", **kwargs
) -> None:
"""
Отправляет прямое сообщение с дополнительной обработкой для sticker и video_note.
Для этих типов медиа отправляется отдельное текстовое сообщение, т.к. они не поддерживают caption.
@@ -213,51 +317,34 @@ async def send_direct_message(bot, chat_id: int, content: MessageContent, extra_
case "sticker":
# Отправляем стикер с отфильтрованными параметрами
sticker_kwargs = filter_kwargs("sticker", kwargs)
await bot.send_sticker(
chat_id=chat_id,
sticker=content.file_id,
**sticker_kwargs
)
await bot.send_sticker(chat_id=chat_id, sticker=content.file_id, **sticker_kwargs)
# Если есть текст с подписью, отправляем отдельно
if content.text or extra_text:
text_to_send = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id,
text_to_send,
**text_kwargs
)
await bot.send_message(chat_id, text_to_send, **text_kwargs)
case "video_note":
# Отправляем видео-заметку с отфильтрованными параметрами
video_note_kwargs = filter_kwargs("video_note", kwargs)
await bot.send_video_note(
chat_id=chat_id,
video_note=content.file_id,
**video_note_kwargs
chat_id=chat_id, video_note=content.file_id, **video_note_kwargs
)
# Если есть текст с подписью, отправляем отдельно
if content.text or extra_text:
text_to_send = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id,
text_to_send,
**text_kwargs
)
await bot.send_message(chat_id, text_to_send, **text_kwargs)
case "text":
# Для текста объединяем с extra_text
final_text = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id=chat_id,
text=final_text,
**text_kwargs
)
await bot.send_message(chat_id=chat_id, text=final_text, **text_kwargs)
case _:
# Для остальных типов медиа используем caption
final_caption = (content.text + extra_text) if content.text else None
await send_message_by_type(
bot, chat_id,
bot,
chat_id,
MessageContent(content.content_type, content.file_id, final_caption),
**kwargs
)
**kwargs,
)
-1
View File
@@ -4,7 +4,6 @@ from typing import Any
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest
from aiogram.types import CallbackQuery
_EXPIRED_CALLBACK_MARKERS = (
"query is too old",
"response timeout expired",
+7 -3
View File
@@ -1,8 +1,8 @@
import logging
from typing import Optional, Tuple
from config.settings import Settings
from bot.services.panel_api_service import PanelApiService
from config.settings import Settings
async def _encrypt_raw_link(settings: Settings, raw_link: str) -> Optional[str]:
@@ -14,7 +14,9 @@ async def _encrypt_raw_link(settings: Settings, raw_link: str) -> Optional[str]:
return None
async def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
async def prepare_config_links(
settings: Settings, raw_link: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:
"""
Build the user-facing connection key and the URL for the connect button.
@@ -38,7 +40,9 @@ async def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> T
display_link = encrypted_payload
button_link = display_link
else:
logging.error("CRYPT4_ENABLED is set but encryption failed; using raw link as fallback.")
logging.error(
"CRYPT4_ENABLED is set but encryption failed; using raw link as fallback."
)
redirect_base = (settings.CRYPT4_REDIRECT_URL or "").strip()
if redirect_base and settings.CRYPT4_ENABLED and display_link:
-2
View File
@@ -34,5 +34,3 @@ def month_start(base_dt: Optional[datetime] = None) -> datetime:
else:
moment = moment.astimezone(timezone.utc)
return datetime(moment.year, moment.month, 1, tzinfo=timezone.utc)
+47 -85
View File
@@ -1,9 +1,10 @@
import asyncio
import logging
from typing import Dict, Any, Callable, Awaitable, Optional
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timedelta
from collections import deque
from typing import Any, Awaitable, Callable, Dict, Optional
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
@@ -16,6 +17,7 @@ from bot.utils.telegram_markup import (
@dataclass
class QueuedMessage:
"""Represents a queued message with all necessary parameters"""
chat_id: int
method_name: str # 'send_message', 'edit_message_text', etc.
kwargs: Dict[str, Any]
@@ -24,7 +26,7 @@ class QueuedMessage:
class MessageQueue:
"""Message queue with rate limiting for Telegram API"""
def __init__(self, messages_per_second: float, burst_size: int = 5):
self.messages_per_second = messages_per_second
self.burst_size = burst_size
@@ -34,31 +36,31 @@ class MessageQueue:
self.delay_between_messages = 1.0 / messages_per_second
self.total_sent = 0
self.total_failed = 0
async def add_message(self, message: QueuedMessage) -> None:
"""Add message to queue"""
self.queue.append(message)
if not self.is_processing:
asyncio.create_task(self._process_queue())
async def _process_queue(self) -> None:
"""Process messages from queue with rate limiting"""
if self.is_processing:
return
self.is_processing = True
try:
while self.queue:
# Check if we need to wait
await self._wait_if_needed()
# Get and process next message
message = self.queue.popleft()
try:
await self._send_message(message)
self._record_send_time()
except TelegramBadRequest as exc:
fallback_message = self._build_profile_link_fallback(message, exc)
if fallback_message:
@@ -78,25 +80,25 @@ class MessageQueue:
f"Failed to send fallback message to {message.chat_id}: {retry_exc}"
)
continue
self.total_failed += 1
logging.error(f"Failed to send queued message to {message.chat_id}: {exc}")
except Exception:
self.total_failed += 1
logging.exception("Failed to send queued message to %s.", message.chat_id)
finally:
self.is_processing = False
async def _wait_if_needed(self) -> None:
"""Wait if we need to respect rate limits"""
if not self.last_send_times:
return
# Calculate time since last message
time_since_last = (datetime.now() - self.last_send_times[-1]).total_seconds()
if time_since_last < self.delay_between_messages:
wait_time = self.delay_between_messages - time_since_last
await asyncio.sleep(wait_time)
@@ -132,7 +134,7 @@ class MessageQueue:
kwargs=fallback_kwargs,
callback=message.callback,
)
async def _send_message(self, message: QueuedMessage) -> Any:
"""Send a single message - to be implemented by subclass"""
raise NotImplementedError("Subclass must implement _send_message")
@@ -140,150 +142,110 @@ class MessageQueue:
class TelegramMessageQueue(MessageQueue):
"""Telegram-specific message queue"""
def __init__(self, bot: Bot, messages_per_second: float, burst_size: int = 5):
super().__init__(messages_per_second, burst_size)
self.bot = bot
async def _send_message(self, message: QueuedMessage) -> Any:
"""Send message using bot method"""
method = getattr(self.bot, message.method_name)
result = await method(chat_id=message.chat_id, **message.kwargs)
# Call callback if provided
if message.callback:
await message.callback(result)
return result
class MessageQueueManager:
"""Manager for different types of message queues"""
def __init__(self, bot: Bot):
self.bot = bot
# Different queues for different types of chats
self.group_queue = TelegramMessageQueue(
bot=bot,
messages_per_second=15/60, # 15 messages per minute for groups
burst_size=3
messages_per_second=15 / 60, # 15 messages per minute for groups
burst_size=3,
)
self.user_queue = TelegramMessageQueue(
bot=bot,
bot=bot,
messages_per_second=25, # 25 messages per second for users
burst_size=10
burst_size=10,
)
def _is_group_chat(self, chat_id: int) -> bool:
"""Check if chat_id belongs to a group or channel"""
return str(chat_id).startswith('-100')
return str(chat_id).startswith("-100")
async def send_message(self, chat_id: int, **kwargs) -> None:
"""Queue a send_message call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_message',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_message", kwargs=kwargs)
await queue.add_message(message)
async def edit_message_text(self, chat_id: int, **kwargs) -> None:
"""Queue an edit_message_text call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='edit_message_text',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="edit_message_text", kwargs=kwargs)
await queue.add_message(message)
async def send_document(self, chat_id: int, **kwargs) -> None:
"""Queue a send_document call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_document',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_document", kwargs=kwargs)
await queue.add_message(message)
async def send_photo(self, chat_id: int, **kwargs) -> None:
"""Queue a send_photo call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_photo',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_photo", kwargs=kwargs)
await queue.add_message(message)
async def send_video(self, chat_id: int, **kwargs) -> None:
"""Queue a send_video call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_video',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_video", kwargs=kwargs)
await queue.add_message(message)
async def send_animation(self, chat_id: int, **kwargs) -> None:
"""Queue a send_animation (GIF) call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_animation',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_animation", kwargs=kwargs)
await queue.add_message(message)
async def send_audio(self, chat_id: int, **kwargs) -> None:
"""Queue a send_audio call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_audio',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_audio", kwargs=kwargs)
await queue.add_message(message)
async def send_voice(self, chat_id: int, **kwargs) -> None:
"""Queue a send_voice call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_voice',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_voice", kwargs=kwargs)
await queue.add_message(message)
async def send_sticker(self, chat_id: int, **kwargs) -> None:
"""Queue a send_sticker call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_sticker',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_sticker", kwargs=kwargs)
await queue.add_message(message)
async def send_video_note(self, chat_id: int, **kwargs) -> None:
"""Queue a send_video_note call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_video_note',
kwargs=kwargs
)
message = QueuedMessage(chat_id=chat_id, method_name="send_video_note", kwargs=kwargs)
await queue.add_message(message)
async def answer_callback_query(self, callback_query_id: str, **kwargs) -> None:
"""Send callback query answer immediately (not rate limited)"""
await self.bot.answer_callback_query(callback_query_id, **kwargs)
def get_queue_stats(self) -> Dict[str, Any]:
"""Get statistics about queues"""
return {
+1
View File
@@ -1,4 +1,5 @@
"""Helpers for Telegram Mini App URLs (subscription webapp)."""
from __future__ import annotations
from typing import Optional
+3 -1
View File
@@ -64,7 +64,9 @@ def request_client_ip(
return forwarded_ip
def ip_in_allowlist(ip_value: Optional[str], allowed_entries: Optional[Sequence[str] | str]) -> bool:
def ip_in_allowlist(
ip_value: Optional[str], allowed_entries: Optional[Sequence[str] | str]
) -> bool:
parsed_ip = _parse_ip(ip_value)
if parsed_ip is None:
return False
+1 -2
View File
@@ -20,8 +20,7 @@ def remove_profile_link_buttons(
button
for button in row
if not (
getattr(button, "url", None)
and str(button.url).startswith(TG_USER_LINK_PREFIX)
getattr(button, "url", None) and str(button.url).startswith(TG_USER_LINK_PREFIX)
)
]
if filtered_row:
+1 -3
View File
@@ -153,9 +153,7 @@ def _normalize_for_detection(value: str) -> str:
normalized = unicodedata.normalize("NFKD", value)
normalized = normalized.translate(_PRE_LOWER_TRANSLATION)
normalized = normalized.lower()
normalized = "".join(
ch for ch in normalized if unicodedata.category(ch) != "Mn"
)
normalized = "".join(ch for ch in normalized if unicodedata.category(ch) != "Mn")
normalized = normalized.translate(_POST_LOWER_TRANSLATION)
normalized = normalized.replace("rn", "m")