Утиита для проверки типа сообщений

This commit is contained in:
2026-02-23 14:24:35 +07:00
parent 150519a2a1
commit e4837cf9c9

613
bot/utils/type_message.py Normal file
View File

@@ -0,0 +1,613 @@
"""
Утилиты для работы с типами контента и чатов
"""
from typing import Final, Optional, Dict, Any
from enum import Enum
from aiogram.types import Message
from aiogram.enums import ContentType, ChatType
__all__ = (
'CHAT_TYPES_RU',
'CONTENT_TYPES_RU',
'CONTENT_EMOJI',
'get_chat_type',
'get_content_type',
'get_content_text',
'get_content_emoji',
'get_media_info',
'has_media',
'has_text',
'format_content_info',
'ContentCategory',
'get_content_category',
'is_private_chat',
'is_group_chat',
'is_channel',
'type_msg',
'type_chat'
)
# ==================== КОНСТАНТЫ ====================
# Типы чатов на русском
CHAT_TYPES_RU: Final[Dict[str, str]] = {
ChatType.PRIVATE: "Личные сообщения",
ChatType.GROUP: "Группа",
ChatType.SUPERGROUP: "Супергруппа",
ChatType.CHANNEL: "Канал",
"private": "Личные сообщения",
"group": "Группа",
"supergroup": "Супергруппа",
"channel": "Канал",
}
# Типы контента на русском
CONTENT_TYPES_RU: Final[Dict[str, str]] = {
# Текст и медиа
ContentType.TEXT: "Текст",
ContentType.ANIMATION: "GIF анимация",
ContentType.AUDIO: "Аудиофайл",
ContentType.DOCUMENT: "Документ",
ContentType.PHOTO: "Фотография",
ContentType.STICKER: "Стикер",
ContentType.VIDEO: "Видео",
ContentType.VIDEO_NOTE: "Видеосообщение",
ContentType.VOICE: "Голосовое сообщение",
# Контакты и локации
ContentType.CONTACT: "Контакт",
ContentType.LOCATION: "Геолокация",
ContentType.VENUE: "Место на карте",
# Игры и развлечения
ContentType.DICE: "Игральная кость",
ContentType.GAME: "Игра",
ContentType.POLL: "Опрос",
# События чата
ContentType.NEW_CHAT_MEMBERS: "Новые участники",
ContentType.LEFT_CHAT_MEMBER: "Участник покинул чат",
ContentType.NEW_CHAT_TITLE: "Изменено название чата",
ContentType.NEW_CHAT_PHOTO: "Изменена аватарка чата",
ContentType.DELETE_CHAT_PHOTO: "Удалена аватарка чата",
ContentType.GROUP_CHAT_CREATED: "Группа создана",
ContentType.SUPERGROUP_CHAT_CREATED: "Супергруппа создана",
ContentType.CHANNEL_CHAT_CREATED: "Канал создан",
ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED: "Изменён таймер автоудаления",
ContentType.MIGRATE_TO_CHAT_ID: "Миграция в супергруппу",
ContentType.MIGRATE_FROM_CHAT_ID: "Миграция из группы",
ContentType.PINNED_MESSAGE: "Закреплено сообщение",
# Платежи
ContentType.INVOICE: "Счёт на оплату",
ContentType.SUCCESSFUL_PAYMENT: "Успешная оплата",
# Другое
ContentType.CONNECTED_WEBSITE: "Подключён сайт",
ContentType.PASSPORT_DATA: "Данные Telegram Passport",
ContentType.PROXIMITY_ALERT_TRIGGERED: "Сработал алерт приближения",
# Видеочаты
ContentType.VIDEO_CHAT_SCHEDULED: "Запланирован видеочат",
ContentType.VIDEO_CHAT_STARTED: "Начался видеочат",
ContentType.VIDEO_CHAT_ENDED: "Завершён видеочат",
ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED: "Приглашены в видеочат",
# Web App
ContentType.WEB_APP_DATA: "Данные Web App",
# Форумы
ContentType.FORUM_TOPIC_CREATED: "Создана тема форума",
ContentType.FORUM_TOPIC_EDITED: "Изменена тема форума",
ContentType.FORUM_TOPIC_CLOSED: "Закрыта тема форума",
ContentType.FORUM_TOPIC_REOPENED: "Открыта тема форума",
ContentType.GENERAL_FORUM_TOPIC_HIDDEN: "Скрыта общая тема",
ContentType.GENERAL_FORUM_TOPIC_UNHIDDEN: "Показана общая тема",
# Розыгрыши
ContentType.GIVEAWAY_CREATED: "Создан розыгрыш",
ContentType.GIVEAWAY: "Розыгрыш",
ContentType.GIVEAWAY_WINNERS: "Победители розыгрыша",
ContentType.GIVEAWAY_COMPLETED: "Завершён розыгрыш",
# Истории и реакции
ContentType.STORY: "История",
}
# Эмодзи для типов контента
CONTENT_EMOJI: Final[Dict[str, str]] = {
ContentType.TEXT: "💬",
ContentType.ANIMATION: "🎞️",
ContentType.AUDIO: "🎵",
ContentType.DOCUMENT: "📄",
ContentType.PHOTO: "📷",
ContentType.STICKER: "🎨",
ContentType.VIDEO: "🎥",
ContentType.VIDEO_NOTE: "🎬",
ContentType.VOICE: "🎤",
ContentType.CONTACT: "👤",
ContentType.LOCATION: "📍",
ContentType.VENUE: "🏢",
ContentType.DICE: "🎲",
ContentType.GAME: "🎮",
ContentType.POLL: "📊",
ContentType.INVOICE: "💰",
ContentType.SUCCESSFUL_PAYMENT: "",
}
class ContentCategory(str, Enum):
"""Категории контента"""
TEXT = "text" # Текстовые сообщения
MEDIA = "media" # Медиа (фото, видео, и т.д.)
FILE = "file" # Файлы и документы
VOICE = "voice" # Голосовые сообщения
LOCATION = "location" # Локации и места
INTERACTION = "interaction" # Игры, опросы, кости
SERVICE = "service" # Служебные сообщения
PAYMENT = "payment" # Платежи
UNKNOWN = "unknown" # Неизвестный тип
# ==================== ОСНОВНЫЕ ФУНКЦИИ ====================
def get_chat_type(message: Message, russian: bool = True) -> str:
"""
Возвращает тип чата.
Args:
message: Объект сообщения
russian: Вернуть на русском языке
Returns:
str: Тип чата
Example:
>>> get_chat_type(message)
'Личные сообщения'
>>> get_chat_type(message, russian=False)
'private'
"""
chat_type = message.chat.type
if russian:
return CHAT_TYPES_RU.get(chat_type, f"Неизвестный тип ({chat_type})")
return chat_type
def get_content_type(message: Message, russian: bool = True) -> str:
"""
Возвращает тип контента сообщения.
Args:
message: Объект сообщения
russian: Вернуть на русском языке
Returns:
str: Тип контента
Example:
>>> get_content_type(message)
'Фотография'
>>> get_content_type(message, russian=False)
'photo'
"""
content_type = message.content_type
if russian:
return CONTENT_TYPES_RU.get(content_type, f"Неизвестный тип ({content_type})")
return content_type
def get_content_emoji(message: Message) -> str:
"""
Возвращает эмодзи для типа контента.
Args:
message: Объект сообщения
Returns:
str: Эмодзи
Example:
>>> get_content_emoji(message)
'📷'
"""
return CONTENT_EMOJI.get(message.content_type, "📎")
def get_content_text(message: Message, max_length: Optional[int] = None) -> Optional[str]:
"""
Извлекает текст из сообщения (текст или caption).
Args:
message: Объект сообщения
max_length: Максимальная длина текста (обрезает если больше)
Returns:
Optional[str]: Текст сообщения или None
Example:
>>> get_content_text(message)
'Привет, мир!'
>>> get_content_text(message) # Фото с подписью
'Красивое фото'
>>> get_content_text(message, max_length=10)
'Привет,...'
"""
text = message.text or message.caption
if text and max_length and len(text) > max_length:
return f"{text[:max_length]}..."
return text
def has_media(message: Message) -> bool:
"""
Проверяет, содержит ли сообщение медиа.
Args:
message: Объект сообщения
Returns:
bool: True если есть медиа
Example:
>>> has_media(message)
True
"""
media_types = {
ContentType.PHOTO,
ContentType.VIDEO,
ContentType.ANIMATION,
ContentType.AUDIO,
ContentType.VOICE,
ContentType.VIDEO_NOTE,
ContentType.DOCUMENT,
ContentType.STICKER
}
return message.content_type in media_types
def has_text(message: Message) -> bool:
"""
Проверяет, есть ли в сообщении текст (или caption).
Args:
message: Объект сообщения
Returns:
bool: True если есть текст
Example:
>>> has_text(message)
True
"""
return bool(message.text or message.caption)
# ==================== ДЕТАЛЬНАЯ ИНФОРМАЦИЯ О МЕДИА ====================
def get_media_info(message: Message) -> Dict[str, Any]:
"""
Возвращает детальную информацию о медиа в сообщении.
Args:
message: Объект сообщения
Returns:
Dict: Словарь с информацией о медиа
Example:
>>> get_media_info(message)
{
'type': 'photo',
'type_ru': 'Фотография',
'emoji': '📷',
'has_caption': True,
'caption': 'Красивое фото',
'file_size': 123456,
'file_size_mb': 0.12,
'width': 1920,
'height': 1080,
'duration': None
}
"""
info = {
'type': message.content_type,
'type_ru': get_content_type(message),
'emoji': get_content_emoji(message),
'has_caption': bool(message.caption),
'caption': message.caption,
'has_text': bool(message.text),
'text': message.text,
}
# Фото
if message.photo:
largest_photo = max(message.photo, key=lambda p: p.file_size or 0)
info.update({
'file_id': largest_photo.file_id,
'file_unique_id': largest_photo.file_unique_id,
'file_size': largest_photo.file_size,
'file_size_kb': round(largest_photo.file_size / 1024, 2) if largest_photo.file_size else None,
'width': largest_photo.width,
'height': largest_photo.height,
'count': len(message.photo) # Количество размеров
})
# Видео
elif message.video:
info.update({
'file_id': message.video.file_id,
'file_unique_id': message.video.file_unique_id,
'file_size': message.video.file_size,
'file_size_mb': round(message.video.file_size / (1024 * 1024), 2) if message.video.file_size else None,
'width': message.video.width,
'height': message.video.height,
'duration': message.video.duration,
'duration_formatted': _format_duration(message.video.duration) if message.video.duration else None,
'mime_type': message.video.mime_type,
'file_name': message.video.file_name
})
# Документ
elif message.document:
info.update({
'file_id': message.document.file_id,
'file_unique_id': message.document.file_unique_id,
'file_size': message.document.file_size,
'file_size_mb': round(message.document.file_size / (1024 * 1024),
2) if message.document.file_size else None,
'file_name': message.document.file_name,
'mime_type': message.document.mime_type
})
# Аудио
elif message.audio:
info.update({
'file_id': message.audio.file_id,
'file_unique_id': message.audio.file_unique_id,
'file_size': message.audio.file_size,
'file_size_mb': round(message.audio.file_size / (1024 * 1024), 2) if message.audio.file_size else None,
'duration': message.audio.duration,
'duration_formatted': _format_duration(message.audio.duration) if message.audio.duration else None,
'performer': message.audio.performer,
'title': message.audio.title,
'mime_type': message.audio.mime_type,
'file_name': message.audio.file_name
})
# Голосовое сообщение
elif message.voice:
info.update({
'file_id': message.voice.file_id,
'file_unique_id': message.voice.file_unique_id,
'file_size': message.voice.file_size,
'file_size_kb': round(message.voice.file_size / 1024, 2) if message.voice.file_size else None,
'duration': message.voice.duration,
'duration_formatted': _format_duration(message.voice.duration) if message.voice.duration else None,
'mime_type': message.voice.mime_type
})
# Видеосообщение
elif message.video_note:
info.update({
'file_id': message.video_note.file_id,
'file_unique_id': message.video_note.file_unique_id,
'file_size': message.video_note.file_size,
'file_size_kb': round(message.video_note.file_size / 1024, 2) if message.video_note.file_size else None,
'duration': message.video_note.duration,
'duration_formatted': _format_duration(
message.video_note.duration) if message.video_note.duration else None,
'length': message.video_note.length # Диаметр
})
# Анимация (GIF)
elif message.animation:
info.update({
'file_id': message.animation.file_id,
'file_unique_id': message.animation.file_unique_id,
'file_size': message.animation.file_size,
'file_size_mb': round(message.animation.file_size / (1024 * 1024),
2) if message.animation.file_size else None,
'width': message.animation.width,
'height': message.animation.height,
'duration': message.animation.duration,
'duration_formatted': _format_duration(message.animation.duration) if message.animation.duration else None,
'mime_type': message.animation.mime_type,
'file_name': message.animation.file_name
})
# Стикер
elif message.sticker:
info.update({
'file_id': message.sticker.file_id,
'file_unique_id': message.sticker.file_unique_id,
'file_size': message.sticker.file_size,
'width': message.sticker.width,
'height': message.sticker.height,
'is_animated': message.sticker.is_animated,
'is_video': message.sticker.is_video,
'emoji': message.sticker.emoji,
'set_name': message.sticker.set_name
})
return info
def format_content_info(message: Message, include_text: bool = True, max_text_length: int = 50) -> str:
"""
Форматирует информацию о контенте в читаемую строку.
Args:
message: Объект сообщения
include_text: Включать текст/caption в описание
max_text_length: Максимальная длина текста
Returns:
str: Отформатированная строка
Example:
>>> format_content_info(message)
'📷 Фотография (1920x1080, 123 KB) + "Красивое фото"'
>>> format_content_info(message)
'🎥 Видео (1920x1080, 5.2 MB, 1:30) + "Смотрите это видео"'
"""
emoji = get_content_emoji(message)
content_type = get_content_type(message)
parts = [f"{emoji} {content_type}"]
# Добавляем детали медиа
if message.photo:
largest = max(message.photo, key=lambda p: p.file_size or 0)
size_kb = largest.file_size / 1024 if largest.file_size else 0
parts.append(f"({largest.width}x{largest.height}, {size_kb:.1f} KB)")
elif message.video:
size_mb = message.video.file_size / (1024 * 1024) if message.video.file_size else 0
duration = _format_duration(message.video.duration) if message.video.duration else "?"
parts.append(f"({message.video.width}x{message.video.height}, {size_mb:.1f} MB, {duration})")
elif message.document:
size_mb = message.document.file_size / (1024 * 1024) if message.document.file_size else 0
file_name = message.document.file_name or "без имени"
parts.append(f'("{file_name}", {size_mb:.2f} MB)')
elif message.audio:
duration = _format_duration(message.audio.duration) if message.audio.duration else "?"
title = message.audio.title or "без названия"
parts.append(f'("{title}", {duration})')
elif message.voice:
duration = _format_duration(message.voice.duration) if message.voice.duration else "?"
parts.append(f"({duration})")
elif message.video_note:
duration = _format_duration(message.video_note.duration) if message.video_note.duration else "?"
parts.append(f"({duration})")
elif message.sticker:
emoji_text = message.sticker.emoji or ""
parts.append(f"({emoji_text})")
# Добавляем текст/caption
if include_text:
text = get_content_text(message, max_length=max_text_length)
if text:
parts.append(f'+ "{text}"')
return ' '.join(parts)
def get_content_category(message: Message) -> ContentCategory:
"""
Определяет категорию контента.
Args:
message: Объект сообщения
Returns:
ContentCategory: Категория контента
Example:
>>> get_content_category(message)
ContentCategory.MEDIA
"""
content_type = message.content_type
# Текст
if content_type == ContentType.TEXT:
return ContentCategory.TEXT
# Медиа
if content_type in {ContentType.PHOTO, ContentType.VIDEO, ContentType.ANIMATION, ContentType.STICKER}:
return ContentCategory.MEDIA
# Файлы
if content_type in {ContentType.DOCUMENT, ContentType.AUDIO}:
return ContentCategory.FILE
# Голосовые
if content_type in {ContentType.VOICE, ContentType.VIDEO_NOTE}:
return ContentCategory.VOICE
# Локации
if content_type in {ContentType.LOCATION, ContentType.VENUE}:
return ContentCategory.LOCATION
# Интерактивные
if content_type in {ContentType.DICE, ContentType.GAME, ContentType.POLL}:
return ContentCategory.INTERACTION
# Платежи
if content_type in {ContentType.INVOICE, ContentType.SUCCESSFUL_PAYMENT}:
return ContentCategory.PAYMENT
# Служебные
if content_type in {
ContentType.NEW_CHAT_MEMBERS,
ContentType.LEFT_CHAT_MEMBER,
ContentType.NEW_CHAT_TITLE,
ContentType.PINNED_MESSAGE
}:
return ContentCategory.SERVICE
return ContentCategory.UNKNOWN
# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
def _format_duration(seconds: int) -> str:
"""
Форматирует длительность в читаемый вид.
Args:
seconds: Длительность в секундах
Returns:
str: Отформатированная строка (MM:SS или HH:MM:SS)
Example:
>>> _format_duration(90)
'1:30'
>>> _format_duration(3661)
'1:01:01'
"""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
secs = seconds % 60
if hours > 0:
return f"{hours}:{minutes:02d}:{secs:02d}"
else:
return f"{minutes}:{secs:02d}"
def is_private_chat(message: Message) -> bool:
"""Проверяет, является ли чат личным"""
return message.chat.type == ChatType.PRIVATE
def is_group_chat(message: Message) -> bool:
"""Проверяет, является ли чат группой"""
return message.chat.type in {ChatType.GROUP, ChatType.SUPERGROUP}
def is_channel(message: Message) -> bool:
"""Проверяет, является ли чат каналом"""
return message.chat.type == ChatType.CHANNEL
# Алиасы для обратной совместимости
type_msg = get_content_type
type_chat = get_chat_type