from pathlib import Path from urllib.parse import urlparse, ParseResult from typing import Optional, Any from secrets import token_urlsafe from pydantic import field_validator, model_validator, Field from pydantic_settings import BaseSettings, SettingsConfigDict from aiogram.types import ChatAdministratorRights class _Settings(BaseSettings): """Настройки бота с комплексной валидацией""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, validate_default=True, ) # ============== ОСНОВНЫЕ ПАРАМЕТРЫ ============== # Токены бота BOT_TOKEN: Optional[str] = None DATABASE_PATH: Optional[str] = "data/banwords.db" # Параметры сообщений PARSE_MODE: str = "HTML" PREFIX: str = "/!.&?" # Разрешения и логирование BOT_EDIT: bool = False START_INFO_CONSOLE: bool = True START_INFO_TO_FILE: bool = True LOG_CONSOLE: bool = True LOG_FILE: bool = True LOG_DIR: Path = Path('Logs') LOG_FILE_INFO: Path = Path('bot_info.log') LOG_ROTATION: str = '100 MB' LOG_RETENTION: str = '7 days' # Вебхук WEBHOOK: bool = False SECRET_TOKEN: Optional[str] = '' WEBHOOK_URL: Optional[str] = None WEBAPP_HOST: str = "0.0.0.0" WEBAPP_PORT: int = 3131 LOG_LEVEL: str = "warning" ACCES_LOG: bool = False # API ключи API_KEY: Optional[str] = None WEB_API_KEY: Optional[str] = None WEATHER_API_KEY: Optional[str] = None # Идентификаторы OWNER_ID: list[int] = [6751720805] ADMIN_ID: list[int] = [] ADMIN_CHAT_ID: int = 0 # Настройки бота BOT_NAME: str = "Бот" BOT_DESCRIPTION: Optional[str] = None BOT_SHORT_DESCRIPTION: Optional[str] = None # ============ АВТОКОММЕНТАРИИ В КАНАЛЕ ============ AUTO_COMMENT_CHANNELS: str = Field( default="", description="ID каналов через запятую" ) AUTO_COMMENT_TEXT: str = Field( default="🔍 Нужна помощь?\n\nИспользуй наш сервис!", description="Текст по умолчанию (HTML)" ) AUTO_COMMENT_BUTTON_TEXT: str = Field( default="🌐 Искать в Google", description="Текст кнопки по умолчанию" ) AUTO_COMMENT_BUTTON_URL: str = Field( default="https://www.google.com", description="URL кнопки по умолчанию" ) AUTO_COMMENT_PHOTO_URL: str = Field( default="https://via.placeholder.com/800x600.png", description="URL фото по умолчанию" ) # Права администратора ANONYMOUS: bool = False MANAGE_CHAT: bool = True CHANGE_INFO: bool = True PROMOTE_MEMBERS: bool = True RESTRICT_MEMBERS: bool = True POST_MESSAGE: bool = True MANAGE_TOPICS: bool = True INVITE_USER: bool = True DELETE_MESSAGES: bool = True MANAGE_VIDEO_CHATS: bool = True EDIT_MESSAGES: bool = True PIN_MESSAGE: bool = True POST_STORIES: bool = True EDIT_STORIES: bool = True DELETE_STORIES: bool = True # Настройки сообщений DISABLE_NOTIFICATION: bool = False PROTECT_CONTENT: bool = False ALLOW_SENDING_WITHOUT_REPLY: bool = True LINK_PREVIEW_IS_DISABLED: bool = False LINK_PREVIEW_PREFER_SMALL_MEDIA: bool = False LINK_PREVIEW_PREFER_LARGE_MEDIA: bool = True LINK_PREVIEW_SHOW_ABOVE_TEXT: bool = True SHOW_CAPTION_ABOVE_MEDIA: bool = False # улучшения ANTI_SPAM: bool = True # ================= ВАЛИДАТОРЫ ================= @field_validator('PARSE_MODE') def validate_parse_mode(cls, v: str) -> str: allowed_modes: set[str] = {"HTML", "Markdown", "MarkdownV2"} if v not in allowed_modes: raise ValueError(f"Недопустимый PARSE_MODE. Допустимые: {', '.join(allowed_modes)}") return v @field_validator('PREFIX') def validate_prefix(cls, v: str) -> str: cleaned: str = ''.join(dict.fromkeys(v)) # Удаление дубликатов с сохранением порядка if len(cleaned) < 1: raise ValueError("PREFIX должен содержать хотя бы один символ") return cleaned @field_validator('LOG_DIR', 'LOG_FILE_INFO', mode='before') def validate_paths(cls, v: Any) -> Path: return Path(v) if isinstance(v, str) else v @field_validator('WEBHOOK_URL') def validate_webhook_url(cls, v: Optional[str]) -> Optional[str]: if v is None: return v parsed: ParseResult = urlparse(v) if not all([parsed.scheme, parsed.netloc]): raise ValueError("Некорректный URL вебхука") if parsed.scheme != 'https': raise ValueError("WEBHOOK_URL должен использовать HTTPS") return v @field_validator('BOT_NAME') def validate_non_empty(cls, v: str) -> str: if not v.strip(): raise ValueError("Поле не может быть пустым") return v @model_validator(mode='after') def validate_bot_token(self) -> "_Settings": if not self.BOT_TOKEN: raise ValueError("Требуется BOT_TOKEN для рабочего режима") return self @model_validator(mode='after') def validate_webhook_config(self) -> "_Settings": if self.WEBHOOK and not self.WEBHOOK_URL: raise ValueError("WEBHOOK_URL обязателен при включенном WEBHOOK") # ✅ Генерация SECRET_TOKEN если не установлен if self.WEBHOOK and not self.SECRET_TOKEN: self.SECRET_TOKEN = token_urlsafe(32) return self @model_validator(mode='after') def validate_logging_paths(self) -> "_Settings": if self.LOG_FILE: self.LOG_DIR.mkdir(parents=True, exist_ok=True) return self @model_validator(mode='after') def set_dynamic_descriptions(self) -> "_Settings": if self.BOT_DESCRIPTION is None: self.BOT_DESCRIPTION = f"Ваш помощник в удивительные миры! Prod. by:『@verdise』" if self.BOT_SHORT_DESCRIPTION is None: self.BOT_SHORT_DESCRIPTION = f"Тех.поддержка: @verdise" return self # ================= СВОЙСТВА ================= @property def AUTO_COMMENT_CHANNELS_LIST(self) -> list[int]: """Преобразует строку ID каналов в список""" if not self.AUTO_COMMENT_CHANNELS: return [] try: return [ int(channel_id.strip()) for channel_id in self.AUTO_COMMENT_CHANNELS.split(",") if channel_id.strip() ] except ValueError: from middleware.loggers import logger # ✅ ДОБАВЬ ИМПОРТ logger.error( "Неверный формат AUTO_COMMENT_CHANNELS", log_type="CONFIG" ) return [] @property def rights(self) -> ChatAdministratorRights: """Права администратора бота""" return ChatAdministratorRights( is_anonymous=self.ANONYMOUS, can_manage_chat=self.MANAGE_CHAT, can_delete_messages=self.DELETE_MESSAGES, can_manage_video_chats=self.MANAGE_VIDEO_CHATS, can_restrict_members=self.RESTRICT_MEMBERS, can_promote_members=self.PROMOTE_MEMBERS, can_change_info=self.CHANGE_INFO, can_invite_users=self.INVITE_USER, can_post_stories=self.POST_STORIES, can_edit_stories=self.EDIT_STORIES, can_delete_stories=self.DELETE_STORIES, can_post_messages=self.POST_MESSAGE, can_edit_messages=self.EDIT_MESSAGES, can_pin_messages=self.PIN_MESSAGE, can_manage_topics=self.MANAGE_TOPICS, ) @property def active_bot_token(self) -> str: """Активный токен бота в зависимости от режима""" if not self.BOT_TOKEN: raise ValueError("Активный токен бота отсутствует") return self.BOT_TOKEN @property def log_dir_absolute(self) -> Path: """Абсолютный путь к директории логов""" return self.LOG_DIR.absolute() @property def super_admin_ids(self) -> set[int]: """Множество ID суперадминов (для банвордов)""" return set(self.OWNER_ID) # ✅ Единственный экземпляр настроек settings = _Settings() # ✅ ОПЦИОНАЛЬНО: Простые константы для обратной совместимости (без дублирования) # Используются только для удобства импорта, но ссылаются на settings BOT_TOKEN = settings.active_bot_token ADMIN_CHAT_ID = settings.ADMIN_CHAT_ID SUPER_ADMIN_IDS = settings.super_admin_ids # Экспорт __all__ = ( 'settings', 'BOT_TOKEN', 'ADMIN_CHAT_ID', 'SUPER_ADMIN_IDS', )