From dfd537b96a7f5b0c5ad71fe1e261b951482382aa Mon Sep 17 00:00:00 2001 From: Verum Date: Mon, 23 Feb 2026 14:36:36 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B5=D0=B9=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/utils/usernames.py | 409 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 bot/utils/usernames.py diff --git a/bot/utils/usernames.py b/bot/utils/usernames.py new file mode 100644 index 0000000..b2eff8c --- /dev/null +++ b/bot/utils/usernames.py @@ -0,0 +1,409 @@ +""" +Утилиты для работы с информацией о пользователях +""" +from typing import Optional, Union +from enum import Enum + +from aiogram.types import Message, CallbackQuery, User, InlineQuery, ChatMemberUpdated + +__all__ = ( + 'get_user_display_name', + 'get_user_mention', + 'get_user_id', + 'username', + 'format_user', + 'UserFormat', + 'is_bot', + 'has_username', + 'is_premium', + 'get_language_code', + 'compare_users', + 'get_user_info_dict' +) + + +class UserFormat(str, Enum): + """Форматы отображения пользователя""" + USERNAME = 'username' # @username или @id123 + FULL_NAME = 'full_name' # Имя Фамилия + MENTION = 'mention' # HTML mention + MENTION_MARKDOWN = 'markdown' # Markdown mention + FIRST_NAME = 'first_name' # Только имя + ID_ONLY = 'id' # Только ID + DETAILED = 'detailed' # @username (Имя Фамилия, ID: 123) + + +# Тип для всех событий с пользователем +EventType = Union[Message, CallbackQuery, InlineQuery, ChatMemberUpdated] + + +def _extract_user(event: EventType) -> Optional[User]: + """ + Извлекает объект User из события. + + Args: + event: Объект события + + Returns: + User или None + """ + if isinstance(event, (Message, CallbackQuery, InlineQuery)): + return event.from_user + elif isinstance(event, ChatMemberUpdated): + return event.from_user or event.new_chat_member.user + + return None + + +def get_user_display_name( + event: EventType, + default: str = "Unknown User" +) -> str: + """ + Возвращает отображаемое имя пользователя (Full Name). + + Args: + event: Объект события (Message, CallbackQuery, и т.д.) + default: Значение по умолчанию если пользователь не найден + + Returns: + str: Полное имя пользователя + + Example: + >> get_user_display_name(message) + 'John Doe' + >> get_user_display_name(message) + 'John' # Если нет фамилии + """ + user = _extract_user(event) + + if not user: + return default + + # Полное имя (приоритет) + if user.full_name: + return user.full_name + + # Только имя + if user.first_name: + return user.first_name + + # Username как запасной вариант + if user.username: + return f"@{user.username}" + + # ID как последний вариант + return f"User {user.id}" + + +def get_user_mention( + event: EventType, + parse_mode: str = 'HTML', + show_username: bool = False +) -> str: + """ + Возвращает упоминание пользователя (кликабельное). + + Args: + event: Объект события + parse_mode: Режим парсинга ('HTML' или 'Markdown') + show_username: Показывать username вместо имени + + Returns: + str: HTML/Markdown упоминание + + Example: + >> get_user_mention(message) + 'John Doe' + + >> get_user_mention(message, parse_mode='Markdown') + '[John Doe](tg://user?id=123456789)' + + >> get_user_mention(message, show_username=True) + '@johndoe' + """ + user = _extract_user(event) + + if not user: + return "Unknown User" + + # Определяем текст для отображения + if show_username and user.username: + display_text = f"@{user.username}" + else: + display_text = user.full_name or user.first_name or f"User {user.id}" + + # Формируем ссылку + user_link = f"tg://user?id={user.id}" + + if parse_mode.upper() == 'HTML': + return f'{display_text}' + elif parse_mode.upper() in ('MARKDOWN', 'MARKDOWNV2'): + # Экранируем специальные символы для Markdown + display_text = display_text.replace('[', '\\[').replace(']', '\\]') + return f'[{display_text}]({user_link})' + else: + return display_text + + +def get_user_id(event: EventType) -> Optional[int]: + """ + Возвращает ID пользователя. + + Args: + event: Объект события + + Returns: + int или None: ID пользователя + + Example: + >> get_user_id(message) + 123456789 + """ + user = _extract_user(event) + return user.id if user else None + + +def username( + event: EventType, + with_at: bool = True, + fallback_to_id: bool = True +) -> str: + """ + Возвращает username пользователя или ID если username отсутствует. + + Это основная функция для получения идентификатора пользователя + в формате @username или @id123. + + Args: + event: Объект события (Message, CallbackQuery, и т.д.) + with_at: Добавлять @ в начало + fallback_to_id: Использовать ID если нет username + + Returns: + str: Username или ID пользователя + + Raises: + ValueError: Если информация о пользователе отсутствует + + Example: + >> username(message) + '@johndoe' + + >> username(message) # Нет username + '@123456789' + + >> username(message, with_at=False) + 'johndoe' + + >> username(message, fallback_to_id=False) + '' # Если нет username + """ + user = _extract_user(event) + + if not user: + raise ValueError("Информация о пользователе отсутствует в событии") + + # Если есть username + if user.username: + return f"@{user.username}" if with_at else user.username + + # Fallback на ID + if fallback_to_id: + return f"@{user.id}" if with_at else str(user.id) + + # Если ничего нет + return "" + + +def format_user( + event: EventType, + format_type: UserFormat = UserFormat.USERNAME, + default: str = "@System" +) -> str: + """ + Универсальная функция форматирования пользователя. + + Args: + event: Объект события + format_type: Тип форматирования (из enum UserFormat) + default: Значение по умолчанию + + Returns: + str: Отформатированная информация о пользователе + + Example: + >> format_user(message, UserFormat.USERNAME) + '@johndoe' + + >> format_user(message, UserFormat.FULL_NAME) + 'John Doe' + + >> format_user(message, UserFormat.MENTION) + 'John Doe' + + >> format_user(message, UserFormat.DETAILED) + '@johndoe (John Doe, ID: 123456789)' + """ + user = _extract_user(event) + + if not user: + return default + + # USERNAME: @username или @id + if format_type == UserFormat.USERNAME: + if user.username: + return f"@{user.username}" + return f"@{user.id}" + + # FULL_NAME: Имя Фамилия + elif format_type == UserFormat.FULL_NAME: + return user.full_name or user.first_name or f"User {user.id}" + + # MENTION: HTML упоминание + elif format_type == UserFormat.MENTION: + display = user.full_name or user.first_name or f"User {user.id}" + return f'{display}' + + # MENTION_MARKDOWN: Markdown упоминание + elif format_type == UserFormat.MENTION_MARKDOWN: + display = user.full_name or user.first_name or f"User {user.id}" + display = display.replace('[', '\\[').replace(']', '\\]') + return f'[{display}](tg://user?id={user.id})' + + # FIRST_NAME: Только имя + elif format_type == UserFormat.FIRST_NAME: + return user.first_name or f"User {user.id}" + + # ID_ONLY: Только ID + elif format_type == UserFormat.ID_ONLY: + return str(user.id) + + # DETAILED: Подробная информация + elif format_type == UserFormat.DETAILED: + parts = [] + + # Username + if user.username: + parts.append(f"@{user.username}") + + # Full name + if user.full_name: + parts.append(f"({user.full_name}") + elif user.first_name: + parts.append(f"({user.first_name}") + + # ID + parts.append(f"ID: {user.id})") + + return ' '.join(parts) if parts else f"User {user.id}" + + # По умолчанию + return default + + +# ================= ДОПОЛНИТЕЛЬНЫЕ УТИЛИТЫ ================= + +def is_bot(event: EventType) -> bool: + """ + Проверяет, является ли пользователь ботом. + + Args: + event: Объект события + + Returns: + bool: True если бот + """ + user = _extract_user(event) + return user.is_bot if user else False + + +def has_username(event: EventType) -> bool: + """ + Проверяет, есть ли у пользователя username. + + Args: + event: Объект события + + Returns: + bool: True если есть username + """ + user = _extract_user(event) + return bool(user and user.username) + + +def is_premium(event: EventType) -> bool: + """ + Проверяет, есть ли у пользователя Telegram Premium. + + Args: + event: Объект события + + Returns: + bool: True если Premium + """ + user = _extract_user(event) + return user.is_premium if user else False + + +def get_language_code(event: EventType) -> Optional[str]: + """ + Возвращает код языка пользователя. + + Args: + event: Объект события + + Returns: + Optional[str]: Код языка ('ru', 'en', и т.д.) + """ + user = _extract_user(event) + return user.language_code if user else None + + +def compare_users(event1: EventType, event2: EventType) -> bool: + """ + Сравнивает двух пользователей по ID. + + Args: + event1: Первое событие + event2: Второе событие + + Returns: + bool: True если это один и тот же пользователь + """ + user1 = _extract_user(event1) + user2 = _extract_user(event2) + + if not user1 or not user2: + return False + + return user1.id == user2.id + + +def get_user_info_dict(event: EventType) -> dict: + """ + Возвращает всю информацию о пользователе в виде словаря. + + Args: + event: Объект события + + Returns: + dict: Словарь с информацией о пользователе + """ + user = _extract_user(event) + + if not user: + return {} + + return { + 'id': user.id, + 'username': user.username, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'full_name': user.full_name, + 'is_bot': user.is_bot, + 'is_premium': user.is_premium, + 'language_code': user.language_code, + 'mention': get_user_mention(event), + 'display_name': get_user_display_name(event) + }