diff --git a/.env.example b/.env.example index 7240f8e..0f157f5 100644 --- a/.env.example +++ b/.env.example @@ -18,11 +18,10 @@ DB_POOL_MAX=10 DB_STATEMENT_TIMEOUT_MS=15000 # Чаты -# Основной чат клана (prod) и тестовый чат -PROD_CHAT_ID= -TEST_CHAT_ID= -# Личный чат для алертов об ошибках -ALERT_CHAT_ID= +# Основной чат клана (доступ в бота — только его участникам) +CHAT_ID= +# Чат для служебных уведомлений/логов (опционально) +ALERT_CHAT_ID=0 # Telegram ID администраторов (через запятую) ADMIN_IDS= diff --git a/DEPLOY.md b/DEPLOY.md index a49f2a4..cb0f64f 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -15,7 +15,7 @@ cd DispelEvilBot ```bash cp env/test.env.example env/test.env -# заполнить: BOT_TOKEN, DB_PASSWORD, тестовые чаты (TEST_CHAT_ID/PROD_CHAT_ID), ADMIN_IDS +# заполнить: BOT_TOKEN, DB_PASSWORD, CHAT_ID (основной чат), ALERT_CHAT_ID (опц.), ADMIN_IDS (опц.) ``` ## 3. Логин в registry (если пакет приватный) diff --git a/bot/handlers/member_events.py b/bot/handlers/member_events.py index d198d84..e7a69f3 100644 --- a/bot/handlers/member_events.py +++ b/bot/handlers/member_events.py @@ -22,7 +22,7 @@ router = Router(name="member_events") async def bot_added_handler(event: ChatMemberUpdated) -> None: """Бот добавлен в чат.""" settings = get_settings() - if event.chat.id not in {settings.prod_chat_id, settings.test_chat_id}: + if event.chat.id not in {settings.chat_id, settings.alert_chat_id}: await event.bot.send_message(chat_id=event.chat.id, text=system.MSG_BOT_FOREIGN_CHAT) await event.bot.leave_chat(event.chat.id) else: @@ -35,6 +35,8 @@ async def welcome_handler(event: ChatMemberUpdated) -> None: user = event.new_chat_member.user if user.is_bot: return + if event.chat.id != get_settings().chat_id: + return await users_service.register_from_event(event) @@ -61,21 +63,16 @@ async def goodbye_handler(event: ChatMemberUpdated) -> None: user = event.from_user if user is None or user.is_bot: return - if event.chat.id == settings.test_chat_id: - return await users_service.delete_user(user.id) logger.info("Удалён пользователь %s после выхода из чата", user.id) + if not settings.alert_chat_id: + return try: await event.bot.send_message( - chat_id=settings.test_chat_id, - text=f"[goodbye_handler]\nУдалил {user.username or user.id}!", + chat_id=settings.alert_chat_id, + text=f"[goodbye] {user.username or user.id} покинул чат {event.chat.title}", ) - if settings.alert_chat_id: - await event.bot.send_message( - chat_id=settings.alert_chat_id, - text=f"Пользователь {user.username or user.id} покинул чат {event.chat.title}", - ) except Exception: logger.exception("Не удалось отправить уведомление о выходе пользователя") diff --git a/bot/services/access.py b/bot/services/access.py index 847ac17..387d881 100644 --- a/bot/services/access.py +++ b/bot/services/access.py @@ -39,15 +39,9 @@ async def _is_member(bot: Bot, user_id: int) -> bool: async def check_user(message: Message) -> bool: - """Пропустить пользователя: проверить членство и обновить его запись. - - В тестовом чате доступ разрешён всегда. - """ + """Пропустить пользователя: доступ только участникам основного чата (или админам).""" settings = get_settings() - if message.chat.id == settings.test_chat_id: - return True - user = message.from_user if user is None: return False @@ -63,18 +57,21 @@ async def check_user(message: Message) -> bool: async def _notify_denied(message: Message) -> None: + """Сообщить в чат логов (если задан) о попытке доступа извне.""" settings = get_settings() + if not settings.alert_chat_id: + return + username = message.from_user.username if message.from_user else None try: await message.bot.send_message( - chat_id=settings.test_chat_id, + chat_id=settings.alert_chat_id, text=f"[check_user]\nСтучится в чат {username}!", ) - if settings.alert_chat_id: - await message.bot.forward_message( - chat_id=settings.alert_chat_id, - from_chat_id=message.chat.id, - message_id=message.message_id, - ) + await message.bot.forward_message( + chat_id=settings.alert_chat_id, + from_chat_id=message.chat.id, + message_id=message.message_id, + ) except Exception: # уведомление не должно ломать обработку logger.exception("Не удалось отправить уведомление о запрете доступа") diff --git a/bot/services/notifications.py b/bot/services/notifications.py index d303f10..a085e30 100644 --- a/bot/services/notifications.py +++ b/bot/services/notifications.py @@ -64,8 +64,9 @@ async def _send_broadcast(bot: Bot, template: str, subscribers: list[dict]) -> N async def _tick(bot: Bot, now: datetime) -> None: settings = get_settings() - if now.minute == 0: - await bot.send_message(chat_id=settings.test_chat_id, text="Я ЖЫФ!") + if now.minute == 0 and settings.alert_chat_id: + # Пинг «жив» в чат логов (если задан). + await bot.send_message(chat_id=settings.alert_chat_id, text="Я ЖЫФ!") for schedule in SCHEDULES: if schedule.matches(now): diff --git a/common/config.py b/common/config.py index 6f3514b..61db8ba 100644 --- a/common/config.py +++ b/common/config.py @@ -37,10 +37,12 @@ class Settings(BaseSettings): db_pool_max: int = 10 db_statement_timeout_ms: int = 15000 - prod_chat_id: int = 0 - test_chat_id: int = 0 + #: Основной чат клана (единственный; контуры разделяются отдельными .env). + chat_id: int = 0 + #: Доп. чат для служебных уведомлений/логов (опционально). alert_chat_id: int = 0 + #: Telegram-ID, которым доступ разрешён в обход проверки членства. admin_ids: list[int] = Field(default_factory=list) timezone: str = "Europe/Moscow" @@ -48,6 +50,14 @@ class Settings(BaseSettings): #: Файл-heartbeat для Docker healthcheck (обновляется ботом периодически). heartbeat_path: str = "/tmp/dispelevilbot.heartbeat" + @field_validator("chat_id", "alert_chat_id", mode="before") + @classmethod + def _empty_int_to_zero(cls, value: object) -> object: + """Пустое значение в .env трактуем как 0 (а не ошибку валидации).""" + if value is None or value == "": + return 0 + return value + @field_validator("admin_ids", mode="before") @classmethod def _parse_admin_ids(cls, value: object) -> object: @@ -65,11 +75,6 @@ class Settings(BaseSettings): def is_prod(self) -> bool: return self.app_env.strip().lower() in {"prod", "production"} - @property - def chat_id(self) -> int: - """Основной чат клана для текущего окружения.""" - return self.prod_chat_id if self.is_prod else self.test_chat_id - @property def tzinfo(self) -> ZoneInfo: return ZoneInfo(self.timezone) diff --git a/env/prod.env.example b/env/prod.env.example index 62a5bb7..0dbd53a 100644 --- a/env/prod.env.example +++ b/env/prod.env.example @@ -23,8 +23,10 @@ DB_POOL_MIN=1 DB_POOL_MAX=10 DB_STATEMENT_TIMEOUT_MS=15000 -PROD_CHAT_ID= -TEST_CHAT_ID= -ALERT_CHAT_ID= +# Основной чат этого контура (доступ в бота — только его участникам) +CHAT_ID= +# Чат для служебных уведомлений/логов (опционально; пусто или 0 — выключено) +ALERT_CHAT_ID=0 +# Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто ADMIN_IDS= TIMEZONE=Europe/Moscow diff --git a/env/test.env.example b/env/test.env.example index 51816ce..65a15b6 100644 --- a/env/test.env.example +++ b/env/test.env.example @@ -21,8 +21,10 @@ DB_POOL_MIN=1 DB_POOL_MAX=10 DB_STATEMENT_TIMEOUT_MS=15000 -PROD_CHAT_ID= -TEST_CHAT_ID= -ALERT_CHAT_ID= +# Основной чат этого контура (доступ в бота — только его участникам) +CHAT_ID= +# Чат для служебных уведомлений/логов (опционально; пусто или 0 — выключено) +ALERT_CHAT_ID=0 +# Telegram-ID админов (доступ в обход проверки членства), через запятую; можно пусто ADMIN_IDS= TIMEZONE=Europe/Moscow