diff --git a/bot/utils/type_message.py b/bot/utils/type_message.py new file mode 100644 index 0000000..4dec0aa --- /dev/null +++ b/bot/utils/type_message.py @@ -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