From ccb47d527f4ece03b0ed51f73fa2c40b2c53bd71 Mon Sep 17 00:00:00 2001 From: UdoChudo Date: Mon, 16 Jun 2025 09:08:46 +0500 Subject: [PATCH] refactor: modularize Telegram bot and add RabbitMQ client foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Рефакторинг Telegram бота на модульную структуру для удобства поддержки и расширения - Создан общий RabbitMQ клиент для Flask и Telegram компонентов - Подготовлена базовая архитектура для будущего масштабирования и новых функций Signed-off-by: UdoChudo --- .dockerignore | 36 +- .gitea/workflows/gitea-ci.yml | 32 ++ Dockerfile | 55 ++-- app/__init__.py | 31 +- app/bot/config.py | 17 - app/bot/constants.py | 12 + app/bot/handlers/__init__.py | 34 ++ app/bot/handlers/active_triggers.py | 49 +++ app/bot/handlers/cancel_input.py | 22 ++ app/bot/handlers/debug.py | 39 +++ app/bot/handlers/help.py | 33 +- app/bot/handlers/main_menu.py | 13 - app/bot/handlers/my_subscriptions.py | 24 ++ app/bot/handlers/notification_switch_mode.py | 72 ++++ app/bot/handlers/registration.py | 14 +- app/bot/handlers/settings.py | 40 ++- app/bot/handlers/start.py | 50 +-- app/bot/handlers/subscribe.py | 46 +++ app/bot/handlers/template_settings.py | 15 + app/bot/handlers/unsubscribe.py | 35 ++ .../__init__.py => i18n/__ini__.py} | 0 app/bot/i18n/messages.py | 12 + app/bot/keyboards/active_triggers.py | 39 +++ app/bot/keyboards/settings_menu.py | 2 +- app/bot/middlewares/user_access.py | 49 --- .../processors/active_triggers_processor.py | 56 ++++ .../processors/my_subscriptions_processor.py | 39 +++ app/bot/processors/subscribe_processor.py | 75 +++++ app/bot/processors/unsubscribe_processor.py | 65 ++++ app/{workers => bot/services}/__init__.py | 0 app/bot/services/mailing_service/__init__.py | 22 ++ app/bot/services/mailing_service/composer.py | 51 +++ app/bot/services/mailing_service/db_utils.py | 38 +++ .../mailing_service/mailing_consumer.py | 151 +++++++++ app/bot/services/mailing_service/parser.py | 19 ++ .../mailing_service/recepient_resolver.py | 8 + app/bot/states.py | 24 ++ app/bot/telezab_bot.py | 28 +- app/bot/utils/__init__.py | 0 app/bot/utils/auth.py | 32 ++ app/bot/utils/cancel.py | 0 app/bot/utils/helpers.py | 55 ++++ app/bot/utils/regions.py | 31 ++ app/bot/utils/tg_audit.py | 30 ++ app/bot/utils/tg_escape_chars.py | 20 ++ app/bot/utils/tg_formatter.py | 35 ++ app/bot/utils/zabbix.py | 119 +++++++ app/extensions/bot_send.py | 25 ++ app/extensions/db.py | 1 + app/extensions/rabbitmq.py | 63 ++++ app/models/userevents.py | 1 + app/routes/api/notifications.py | 23 +- app/services/bot/bot_database.py | 72 ---- app/services/notifications_service.py | 56 ++-- app/services/users_service.py | 6 +- app/workers/rabbitmq_worker.py | 57 ---- backend_bot.py | 198 ----------- backend_locks.py | 4 - backend_zabbix.py | 166 ---------- bot_database.py | 80 ----- config.py | 67 ++-- handlers.py | 71 ---- requirements.txt | 5 +- run_flask.py | 7 + run_telegram.py | 19 ++ supervisord.conf | 23 -- telezab.py | 310 ------------------ utilities/log_manager.py | 133 -------- utilities/notification_manager.py | 42 --- utilities/rabbitmq.py | 122 ------- utilities/telegram_utilities.py | 116 ------- utilities/user_state_manager.py | 16 - 72 files changed, 1677 insertions(+), 1675 deletions(-) create mode 100644 .gitea/workflows/gitea-ci.yml delete mode 100644 app/bot/config.py create mode 100644 app/bot/constants.py create mode 100644 app/bot/handlers/active_triggers.py create mode 100644 app/bot/handlers/cancel_input.py create mode 100644 app/bot/handlers/debug.py delete mode 100644 app/bot/handlers/main_menu.py create mode 100644 app/bot/handlers/my_subscriptions.py create mode 100644 app/bot/handlers/notification_switch_mode.py create mode 100644 app/bot/handlers/subscribe.py create mode 100644 app/bot/handlers/template_settings.py create mode 100644 app/bot/handlers/unsubscribe.py rename app/bot/{middlewares/__init__.py => i18n/__ini__.py} (100%) create mode 100644 app/bot/i18n/messages.py create mode 100644 app/bot/keyboards/active_triggers.py delete mode 100644 app/bot/middlewares/user_access.py create mode 100644 app/bot/processors/active_triggers_processor.py create mode 100644 app/bot/processors/my_subscriptions_processor.py create mode 100644 app/bot/processors/subscribe_processor.py create mode 100644 app/bot/processors/unsubscribe_processor.py rename app/{workers => bot/services}/__init__.py (100%) create mode 100644 app/bot/services/mailing_service/__init__.py create mode 100644 app/bot/services/mailing_service/composer.py create mode 100644 app/bot/services/mailing_service/db_utils.py create mode 100644 app/bot/services/mailing_service/mailing_consumer.py create mode 100644 app/bot/services/mailing_service/parser.py create mode 100644 app/bot/services/mailing_service/recepient_resolver.py create mode 100644 app/bot/utils/__init__.py create mode 100644 app/bot/utils/auth.py create mode 100644 app/bot/utils/cancel.py create mode 100644 app/bot/utils/helpers.py create mode 100644 app/bot/utils/regions.py create mode 100644 app/bot/utils/tg_audit.py create mode 100644 app/bot/utils/tg_escape_chars.py create mode 100644 app/bot/utils/tg_formatter.py create mode 100644 app/bot/utils/zabbix.py create mode 100644 app/extensions/bot_send.py create mode 100644 app/extensions/rabbitmq.py delete mode 100644 app/services/bot/bot_database.py delete mode 100644 app/workers/rabbitmq_worker.py delete mode 100644 backend_bot.py delete mode 100644 backend_locks.py delete mode 100644 backend_zabbix.py delete mode 100644 bot_database.py delete mode 100644 handlers.py create mode 100644 run_flask.py create mode 100644 run_telegram.py delete mode 100644 supervisord.conf delete mode 100644 telezab.py delete mode 100644 utilities/log_manager.py delete mode 100644 utilities/notification_manager.py delete mode 100644 utilities/rabbitmq.py delete mode 100644 utilities/telegram_utilities.py delete mode 100644 utilities/user_state_manager.py diff --git a/.dockerignore b/.dockerignore index e776b30..843165d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,4 +9,38 @@ /db/ /db/telezab.db /trash/ -/venv3.12.3/ \ No newline at end of file +/venv3.12.3/ +.gitea/ +.github/ +.gitlab/ +# Python bytecode +__pycache__/ +*.py[cod] +*.pyo + +# Editor swap/temp files +*.swp +*.swo +*.bak +*~ + +# SQLite journals (если используется) +*.db-journal + +# Docker-related +Dockerfile.* +docker-compose.override.yml + +# Test/coverage artifacts +*.coverage +htmlcov/ +.cache/ +.coverage.* +.nox/ +.tox/ +.pytest_cache/ + +# Build artifacts +build/ +dist/ +*.egg-info/ diff --git a/.gitea/workflows/gitea-ci.yml b/.gitea/workflows/gitea-ci.yml new file mode 100644 index 0000000..2495c6a --- /dev/null +++ b/.gitea/workflows/gitea-ci.yml @@ -0,0 +1,32 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + env: + IMAGE_VERSION: "0.9.0" # тут можно задать фиксированную версию или подставить, например, дату/хеш + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Gitea Registry + run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.udochudo.ru -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin + + - name: Build and push Flask image + run: | + docker build --target flask -t git.udochudo.ru/udochudo/telezab-flask:${IMAGE_VERSION} . + docker push git.udochudo.ru/udochudo/telezab-flask:${IMAGE_VERSION} + + - name: Build and push Telegram bot image + run: | + docker build --target telegram -t git.udochudo.ru/udochudo/telezab-bot:${IMAGE_VERSION} . + docker push git.udochudo.ru/udochudo/telezab-bot:${IMAGE_VERSION} diff --git a/Dockerfile b/Dockerfile index 6cb4853..1864d13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,44 @@ -FROM python:3.13.1-slim +# syntax=docker/dockerfile:1.4 +FROM python:3.13.1-slim AS base LABEL authors="UdoChudo" -# Установим необходимые пакеты + +# Установка системных зависимостей и очистка RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev \ gcc \ + libpq-dev \ tzdata \ sqlite3 \ - curl \ - telnet \ - supervisor \ - && rm -rf /var/lib/apt/lists/* + && apt-get clean && rm -rf /var/lib/apt/lists/* + +ENV TZ=Europe/Moscow +ENV PYTHONUNBUFFERED=1 -# Установим рабочую директорию WORKDIR /app -# Скопируем файлы проекта -COPY . /app - -# Копируем конфигурацию supervisord -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Установим зависимости проекта -RUN mkdir -p /app/logs +COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir gunicorn==23.0.0 -# Откроем порт для нашего приложения + +COPY . . + +# ==================== +# Образ для Flask +# ==================== +FROM base AS flask + +ENV APP_TYPE=flask +ENV FLASK_APP=app + EXPOSE 5000 -ENV TZ=Europe/Moscow -ENV FLASK_APP telezab.py -ENV PYTHONUNBUFFERED 1 +ENTRYPOINT ["/bin/sh", "-c"] +CMD ["gunicorn --access-logfile - --error-logfile - -b 0.0.0.0:5000 'app:create_app()'"] +# ==================== +# Образ для Telegram бота +# ==================== +FROM base AS telegram -# Указываем команду для запуска supervisord -CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file +ENV APP_TYPE=telegram + +ENTRYPOINT ["/bin/sh", "-c"] +CMD ["python run_telegram.py"] diff --git a/app/__init__.py b/app/__init__.py index 06baa5e..bb9ce53 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,32 +8,33 @@ from app.models.user import User from app.routes import register_blueprints from app.extensions.auth_ext import init_auth, login_manager -import config +from config import Config from app.routes.dashboard import dashboard_bp # from backend.api import bp_api -from config import TZ + # noinspection SpellCheckingInspection -def create_app(): +def create_app() -> Flask: app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates') - app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI - app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False - app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ - app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True - app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True - app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE - app.config['SESSION_REFRESH_EACH_REQUEST'] = False - app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME - app.config['SESSION_COOKIE_MAX_AGE'] = 3600 - app.config['TIMEZONE'] = TZ + app.config.from_object(Config) + + # app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI + # app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + # app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ + # app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True + # app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True + # app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE + # app.config['SESSION_REFRESH_EACH_REQUEST'] = False + # app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME + # app.config['SESSION_COOKIE_MAX_AGE'] = 3600 + # app.config['TIMEZONE'] = TZ # Инициализация расширений db.init_app(app) login_manager.init_app(app) init_auth(app) - # Инициализация AuditLogger с передачей db.session app.audit_logger = AuditLogger(db.session) @@ -78,5 +79,3 @@ def create_app(): return app -app = create_app() - diff --git a/app/bot/config.py b/app/bot/config.py deleted file mode 100644 index 6be2774..0000000 --- a/app/bot/config.py +++ /dev/null @@ -1,17 +0,0 @@ -import os - -#Дебаг режим -DEV = os.getenv('DEV') -#Токены и URL'ы -BOT_TOKEN = os.getenv('TELEGRAM_TOKEN') -ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') -ZABBIX_URL = os.getenv('ZABBIX_URL') -SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" -HELP_URL = "https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183" -DB_PATH = 'db/telezab.db' - -RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') -RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') -RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') -RABBITMQ_QUEUE = 'telegram_notifications' -RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" diff --git a/app/bot/constants.py b/app/bot/constants.py new file mode 100644 index 0000000..6da7dbf --- /dev/null +++ b/app/bot/constants.py @@ -0,0 +1,12 @@ +# app/bot/constants.py +from enum import Enum, auto + +class UserStates(Enum): + REGISTRATION = auto() + MAIN_MENU = auto() + SETTINGS_MENU = auto() + SUBSCRIBE = auto() + WAITING_INPUT = auto() + UNSUBSCRIBE = auto() + NOTIFICATION_MODE_SELECTION = auto() + SYSTEM_SELECTION = auto() diff --git a/app/bot/handlers/__init__.py b/app/bot/handlers/__init__.py index e69de29..1def746 100644 --- a/app/bot/handlers/__init__.py +++ b/app/bot/handlers/__init__.py @@ -0,0 +1,34 @@ +from . import subscribe, active_triggers +from . import unsubscribe +from . import my_subscriptions +from . import cancel_input +from . import notification_switch_mode +from . import help +from . import registration +from . import settings +from . import start +from . import debug +from ..states import UserStateManager + +state_manager = UserStateManager() +#Регистрация message_handler кнопок и команд +def register_handlers(bot, app): + #Главная кнопка + start.register_handlers(bot, app, state_manager) + #Кнопки настроек + my_subscriptions.register_handlers(bot, app, state_manager) + subscribe.register_handlers(bot, app, state_manager) + unsubscribe.register_handlers(bot, app, state_manager) + notification_switch_mode.register_handlers(bot, app, state_manager) + #Кнопки главного меню + help.register_handlers(bot, app, state_manager) + registration.register_handlers(bot, state_manager) + settings.register_handlers(bot, app, state_manager) + debug.register_handlers(bot, app, state_manager) + active_triggers.register_active_triggers(bot, app, state_manager) + +#Регистрация callback_data кнопок +def register_callbacks(bot, app): + notification_switch_mode.register_callback_notification(bot, app, state_manager) + active_triggers.register_callbacks_active_triggers(bot, app, state_manager) + cancel_input.register_callback_cancel_input(bot,state_manager) \ No newline at end of file diff --git a/app/bot/handlers/active_triggers.py b/app/bot/handlers/active_triggers.py new file mode 100644 index 0000000..f53eb17 --- /dev/null +++ b/app/bot/handlers/active_triggers.py @@ -0,0 +1,49 @@ +from telebot.types import Message, CallbackQuery + +from app.bot.keyboards.active_triggers import create_region_keyboard +from app.bot.utils.regions import get_sorted_regions_plain +from app.bot.processors.active_triggers_processor import ( + process_region_selection, + process_group_selection, + process_all_groups_request, +) + + +def register_active_triggers(bot, app, state_manager): + @bot.message_handler(commands=['active_triggers']) + @bot.message_handler(func=lambda m: m.text == "Активные проблемы") + def handle_active_triggers(message: Message): + with app.app_context(): + regions = get_sorted_regions_plain() + markup = create_region_keyboard(regions, 0) + bot.send_message(message.chat.id, "Выберите регион для получения активных событий:", reply_markup=markup) + +def register_callbacks_active_triggers(bot,app,state_manager): + @bot.callback_query_handler(func=lambda c: c.data.startswith("region_")) + def region_selected(callback_query: CallbackQuery): + region_id = callback_query.data.split("_")[1] + process_region_selection(callback_query.message.chat.id,bot, region_id) + + @bot.callback_query_handler(func=lambda c: c.data.startswith("group_")) + def group_selected(callback_query: CallbackQuery): + group_id = callback_query.data.split("_")[1] + process_group_selection(callback_query.message.chat.id,bot, group_id) + + @bot.callback_query_handler(func=lambda c: c.data.startswith("all_groups_")) + def all_groups_selected(callback_query: CallbackQuery): + region_id = callback_query.data.split("_")[2] + process_all_groups_request(callback_query.message.chat.id,bot, region_id) + + @bot.callback_query_handler(func=lambda c: c.data.startswith("regions_page_")) + def regions_page_selected(callback_query: CallbackQuery): + page = int(callback_query.data.split("_")[-1]) + with app.app_context(): + regions = get_sorted_regions_plain() + markup = create_region_keyboard(regions, page) + bot.edit_message_reply_markup( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + reply_markup=markup + ) + bot.answer_callback_query(callback_query.id) # обязательно, чтобы убрать "часики" + diff --git a/app/bot/handlers/cancel_input.py b/app/bot/handlers/cancel_input.py new file mode 100644 index 0000000..f46dfd4 --- /dev/null +++ b/app/bot/handlers/cancel_input.py @@ -0,0 +1,22 @@ +from telebot.types import CallbackQuery +from telebot import TeleBot +from app.bot.constants import UserStates +from app.bot.keyboards.main_menu import get_main_menu +from app.bot.states import UserStateManager + +def register_callback_cancel_input(bot: TeleBot, state_manager: UserStateManager): + + @bot.callback_query_handler(func=lambda call: call.data == "cancel_input") + def handle_cancel_input(call: CallbackQuery): + chat_id = call.message.chat.id + message_id = call.message.message_id + + # Сброс состояния + state_manager.set_state(chat_id, UserStates.MAIN_MENU) + + # Удаляем сообщение с кнопкой "Отмена" + bot.delete_message(chat_id, message_id) + bot.answer_callback_query(call.id) + bot.clear_step_handler_by_chat_id(chat_id) + # Отправляем главное меню + bot.send_message(chat_id, "❌ Действие отменено.", reply_markup=get_main_menu()) diff --git a/app/bot/handlers/debug.py b/app/bot/handlers/debug.py new file mode 100644 index 0000000..61dfbc8 --- /dev/null +++ b/app/bot/handlers/debug.py @@ -0,0 +1,39 @@ +import logging + +from telebot import logger +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton, Message + +from config import ADMINS_LIST +LOG_LEVELS = { + "🔴 ERROR": logging.ERROR, + "🟠 WARNING": logging.WARNING, + "🟢 INFO": logging.INFO, + "🔵 DEBUG": logging.DEBUG +} + +def register_handlers(bot,app, state_manager): + @bot.message_handler(commands=['debug'], func=lambda message: message.chat.id in ADMINS_LIST) + def debug_handler(message): + + chat_id = message.chat.id + markup = InlineKeyboardMarkup(row_width=1) + buttons = [InlineKeyboardButton(text=level, callback_data=f"setlog_{level}") for level in LOG_LEVELS] + cancel_button = InlineKeyboardButton(text="Отмена", callback_data="cancel_input") + markup.add(*buttons) + markup.add(cancel_button) + bot.send_message(chat_id, "Выберите уровень логирования", reply_markup=markup) + + @bot.callback_query_handler(func=lambda call: call.data.startswith("setlog_")) + def handle_log_level_callback(call): + message_id = call.message.message_id + level_text = call.data.replace("setlog_", "") + if level_text in LOG_LEVELS: + level = LOG_LEVELS[level_text] + logger.setLevel(level) + for handler in logger.handlers: + handler.setLevel(level) + bot.answer_callback_query(call.id, f"✅ Уровень логирования: {level_text}") + bot.delete_message(call.message.chat.id, message_id) + bot.send_message(call.message.chat.id, f"📋 Логгер переведён в режим: {level_text}") + else: + bot.answer_callback_query(call.id, "❌ Неизвестный уровень логирования") \ No newline at end of file diff --git a/app/bot/handlers/help.py b/app/bot/handlers/help.py index 67dd053..ec3113f 100644 --- a/app/bot/handlers/help.py +++ b/app/bot/handlers/help.py @@ -1,15 +1,30 @@ # app/bot/handlers/help.py +from flask import Flask from telebot.types import Message -from app.bot.config import HELP_URL +from telebot import logger, TeleBot -def register_handlers(bot): +from app.bot.constants import UserStates +from app.bot.keyboards.main_menu import get_main_menu +from app.bot.states import UserStateManager +from app.bot.utils.auth import auth +from config import HELP_URL + +def register_handlers(bot: TeleBot,app: Flask,state_manager: UserStateManager): @bot.message_handler(commands=['help']) @bot.message_handler(func=lambda msg: msg.text == "Помощь") def handle_help(message: Message): - help_text = ( - '/start - Показать меню бота\n' - 'Настройки - Перейти в режим настроек и управления подписками\n' - 'Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n' - f'Помощь - Описание всех возможностей бота' - ) - bot.send_message(message.chat.id, help_text, parse_mode="HTML") + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + with app.app_context(): + if not auth(chat_id, app): + bot.send_message(chat_id, "❌ Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + help_text = ( + 'ℹ️/start - Показать меню бота\n' + 'ℹ️Настройки - Перейти в режим настроек и управления подписками\n' + 'ℹ️Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n' + f'ℹ️Помощь - Описание всех возможностей бота') + + bot.send_message(message.chat.id, help_text, parse_mode="HTML", reply_markup=get_main_menu()) diff --git a/app/bot/handlers/main_menu.py b/app/bot/handlers/main_menu.py deleted file mode 100644 index 9749e5e..0000000 --- a/app/bot/handlers/main_menu.py +++ /dev/null @@ -1,13 +0,0 @@ -# app/bot/handlers/main_menu.py -from telebot.types import Message -from app.bot.keyboards.settings_menu import get_settings_menu - -def register_handlers(bot): - @bot.message_handler(func=lambda msg: msg.text == "Настройки") - - def handle_settings_menu(message: Message): - bot.send_message( - message.chat.id, - "Меню настроек:", - reply_markup=get_settings_menu() - ) diff --git a/app/bot/handlers/my_subscriptions.py b/app/bot/handlers/my_subscriptions.py new file mode 100644 index 0000000..75de0a3 --- /dev/null +++ b/app/bot/handlers/my_subscriptions.py @@ -0,0 +1,24 @@ +import telebot +from flask import Flask +from telebot import TeleBot +from telebot.types import Message +from telebot import logger +from app.bot.constants import UserStates +from app.bot.states import UserStateManager +from app.bot.processors.my_subscriptions_processor import handle_my_subscriptions +from app.bot.utils.auth import auth + + +def register_handlers(bot: TeleBot,app: Flask, state_manager: UserStateManager): + @bot.message_handler(commands=['subscribes']) + @bot.message_handler(func=lambda msg: msg.text == "Мои подписки") + def handle_my_subscriptions_button(message: Message): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + with app.app_context(): + if not auth(chat_id, app): + bot.send_message(chat_id, "❌ Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + handle_my_subscriptions(message, bot,app, state_manager) \ No newline at end of file diff --git a/app/bot/handlers/notification_switch_mode.py b/app/bot/handlers/notification_switch_mode.py new file mode 100644 index 0000000..4874201 --- /dev/null +++ b/app/bot/handlers/notification_switch_mode.py @@ -0,0 +1,72 @@ +import telebot +from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from app.bot.keyboards.main_menu import get_main_menu +from app import Subscriptions, db +from app.bot.constants import UserStates +from app.bot.keyboards.settings_menu import get_settings_menu +from app.bot.utils.auth import auth +from app.bot.utils.tg_audit import log_user_event + + +def register_handlers(bot, app, state_manager): + @bot.message_handler(commands=['notification_mode']) + @bot.message_handler(func=lambda message: message.text == 'Режим уведомлений') + def handle_notification_mode_button(message: Message): + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + chat_id = message.chat.id + with app.app_context(): + if not auth(chat_id, app): + bot.send_message(chat_id, "❌ Вы не авторизованы для использования этого бота.") + telebot.logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + # Отправляем клавиатуру выбора режима уведомлений + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton(text="⛔️ Критические события", callback_data="notification_mode_disaster")) + markup.add(InlineKeyboardButton(text="⚠️ Все события", callback_data="notification_mode_all")) + + bot.send_message(chat_id, + 'Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n' + '1.⛔️Критические события (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\n' + 'В Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n' + '2.⚠️Все события (По умолчанию) - критические события, а также события Zabbix высокого ("HIGH") приоритета, ' + 'имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.', + reply_markup=markup, parse_mode="HTML") + +def register_callback_notification(bot, app, state_manager): + @bot.callback_query_handler(func=lambda call: call.data.startswith("notification_mode_")) + def handle_notification_switch_callback(call): + chat_id = call.message.chat.id + message_id = call.message.message_id + mode = call.data.split("_")[2] + username = f"@{call.from_user.username}" if call.from_user.username else "N/A" + + bot.delete_message(chat_id, message_id) + + # Обновляем режим уведомлений + disaster_only = True if mode == "disaster" else False + + with app.app_context(): + if not auth(chat_id, app): + bot.send_message(chat_id, "❌ Вы не авторизованы для использования этого бота.") + telebot.logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + try: + all_subscriptions = Subscriptions.query.filter_by(chat_id=chat_id).all() + for subscription in all_subscriptions: + subscription.disaster_only = disaster_only + db.session.commit() + + mode_text_emoji = "⛔️ Критические события" if disaster_only else "⚠️ Все события" + mode_text = "Критические события" if disaster_only else "Все события" + bot.send_message(chat_id, f"✅ Режим уведомлений успешно изменён на:\n {mode_text_emoji}",reply_markup=get_settings_menu()) + log_user_event(chat_id, app, username, f"Режим уведомлений изменился на: {mode_text}") + state_manager.set_state(chat_id, UserStates.SETTINGS_MENU) + + except Exception as e: + telebot.logger.error(e) + bot.send_message(chat_id, f"❌ Произошла ошибка при изменении режима уведомлений.", reply_markup=get_main_menu()) + + + diff --git a/app/bot/handlers/registration.py b/app/bot/handlers/registration.py index c2eb17b..c2eaa73 100644 --- a/app/bot/handlers/registration.py +++ b/app/bot/handlers/registration.py @@ -1,21 +1,19 @@ from telebot.types import Message -from app.bot.config import SUPPORT_EMAIL + +from app.bot.states import UserStateManager +from config import SUPPORT_EMAIL -def register_handlers(bot): +def register_handlers(bot,state_manager: UserStateManager): @bot.message_handler(func=lambda msg: msg.text == "Регистрация") def handle_registration(message: Message): chat_id = message.chat.id - username = message.from_user.username - if username: - username = f"@{username}" - else: - username = "N/A" + username = f"{message.from_user.username}" if message.from_user.username else "N/A" text = ( f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n' f'В теме письма указать "Подтверждение регистрации в телеграм-боте TeleZab".\n' f'В теле письма указать:\n' f'1. ФИО\n' f'2. Ваш Chat ID: {chat_id}\n' - f'3. Ваше имя пользователя: {username}') + f'3. Ваше имя пользователя: @{username}') bot.send_message(chat_id, text, parse_mode="HTML") \ No newline at end of file diff --git a/app/bot/handlers/settings.py b/app/bot/handlers/settings.py index d2960a1..09c1c8a 100644 --- a/app/bot/handlers/settings.py +++ b/app/bot/handlers/settings.py @@ -1,25 +1,29 @@ -# app/bot/handlers/settings.py +# app/bot/handlers/main_menu.py from telebot.types import Message +from telebot import logger +from app.bot.constants import UserStates from app.bot.keyboards.main_menu import get_main_menu from app.bot.keyboards.settings_menu import get_settings_menu +from app.bot.states import UserStateManager +from app.bot.utils.auth import auth -def register_handlers(bot): - @bot.message_handler(func=lambda msg: msg.text == "Подписаться") - def handle_subscribe(message: Message): - bot.send_message(message.chat.id, "🔔 Функция подписки ещё не реализована.") - @bot.message_handler(func=lambda msg: msg.text == "Отписаться") - def handle_unsubscribe(message: Message): - bot.send_message(message.chat.id, "🔕 Функция отписки ещё не реализована.") - - @bot.message_handler(func=lambda msg: msg.text == "Мои подписки") - def handle_my_subscriptions(message: Message): - bot.send_message(message.chat.id, "📄 Отображение подписок пока не реализовано.") - - @bot.message_handler(func=lambda msg: msg.text == "Режим уведомлений") - def handle_notify_mode(message: Message): - bot.send_message(message.chat.id, "⚙️ Настройка режима уведомлений пока не реализована.") +def register_handlers(bot,app, state_manager: UserStateManager): + @bot.message_handler(commands=['settings']) + @bot.message_handler(func=lambda msg: msg.text == "Настройки") + def handle_settings_menu(message: Message): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + with app.app_context(): + if not auth(chat_id, app): + bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + else: + state_manager.set_state(chat_id, UserStates.SETTINGS_MENU) + bot.send_message(message.chat.id,"Меню настроек:",reply_markup=get_settings_menu()) @bot.message_handler(func=lambda msg: msg.text == "Назад") - def handle_back(message: Message): - bot.send_message(message.chat.id, "Возврат в главное меню", reply_markup=get_main_menu()) + def handle_back_button(message: Message): + bot.send_message(message.chat.id,"Главное меню", reply_markup=get_main_menu()) \ No newline at end of file diff --git a/app/bot/handlers/start.py b/app/bot/handlers/start.py index d47cc9c..cf5f6a9 100644 --- a/app/bot/handlers/start.py +++ b/app/bot/handlers/start.py @@ -1,41 +1,27 @@ # app/bot/handlers/start.py -from telebot.types import Message, ReplyKeyboardMarkup, KeyboardButton +from telebot.types import Message +from telebot import logger from app.bot.keyboards.main_menu import get_main_menu +from app.bot.constants import UserStates +from app.bot.states import UserStateManager +from app.bot.utils.auth import check_registration -def register_handlers(bot): + +def register_handlers(bot,app, state_manager: UserStateManager): @bot.message_handler(commands=['start']) - def start_handler(message, data=None): + @bot.message_handler(func=lambda msg: msg.text == "Продолжить" and state_manager.get_state(msg.chat.id) == UserStates.REGISTRATION) + def start_handler(message: Message): chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + user = check_registration(bot, message,app) + if not user: + state_manager.set_state(chat_id, UserStates.REGISTRATION) + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + return + else: + state_manager.set_state(chat_id, UserStates.MAIN_MENU) + bot.send_message(chat_id, f"👋 Привет, {username}!", reply_markup=get_main_menu()) - if data: - if data.get('user_verified'): - user = data['user'] - bot.send_message( - chat_id, - f"👋 Привет, {user.user_email}!\nВыберите действие из меню:", - reply_markup=get_main_menu() - ) - return - elif data.get('user_blocked'): - bot.send_message( - chat_id, - "🚫 Ваш аккаунт заблокирован.\n" - "Пожалуйста, обратитесь к администратору." - ) - return - elif data.get('user_not_found'): - keyboard = ReplyKeyboardMarkup(resize_keyboard=True) - keyboard.add(KeyboardButton("Регистрация")) - bot.send_message( - chat_id, - "👋 Добро пожаловать!\n\n" - "❗ Вы не зарегистрированы в системе.\n" - "Пожалуйста, нажмите кнопку ниже для регистрации.", - reply_markup=keyboard - ) - return - # fallback - bot.send_message(chat_id, "Произошла ошибка. Попробуйте позже.") diff --git a/app/bot/handlers/subscribe.py b/app/bot/handlers/subscribe.py new file mode 100644 index 0000000..cf14c19 --- /dev/null +++ b/app/bot/handlers/subscribe.py @@ -0,0 +1,46 @@ + +from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton + +from app import Subscriptions +from app.bot.constants import UserStates +from app.bot.processors.subscribe_processor import process_subscription_button +from app.bot.states import UserStateManager +from app.bot.utils.auth import auth +from app.bot.utils.regions import get_sorted_regions, format_regions_list, format_regions_list_marked +from telebot import TeleBot, logger + + +def register_handlers(bot: TeleBot, app, state_manager: UserStateManager): + @bot.message_handler(commands=['subscribe']) + @bot.message_handler(func=lambda msg: msg.text == "Подписаться") + def handle_subscribe_button(message: Message): + with app.app_context(): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + if not auth(chat_id, app): + bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + else: + state_manager.set_state(chat_id, UserStates.WAITING_INPUT) + + # Получаем регионы + regions = get_sorted_regions() + + # Получаем список подписанных регионов пользователя + subscribed = {s.region_id for s in Subscriptions.query.filter_by(chat_id=chat_id, active=True).all()} + + + # Формируем строку с пометками + regions_text = format_regions_list_marked(regions, subscribed) + + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton(text="Отмена", callback_data="cancel_input")) + + bot_message = bot.send_message(chat_id, + f"Введите номер(а) региона(ов) через запятую для подписки:\n\n{regions_text}", + reply_markup=markup) + + bot.register_next_step_handler(message, process_subscription_button, app, bot, chat_id, state_manager, bot_message.message_id) + diff --git a/app/bot/handlers/template_settings.py b/app/bot/handlers/template_settings.py new file mode 100644 index 0000000..325a78d --- /dev/null +++ b/app/bot/handlers/template_settings.py @@ -0,0 +1,15 @@ +# app/bot/handlers/settings.py +from telebot.types import Message +from app.bot.keyboards.main_menu import get_main_menu +from app.bot.keyboards.settings_menu import get_settings_menu + +def register_handlers(bot,app): + + + @bot.message_handler(func=lambda msg: msg.text == "Режим уведомлений") + def handle_notify_mode(message: Message): + bot.send_message(message.chat.id, "⚙️ Настройка режима уведомлений пока не реализована.") + + @bot.message_handler(func=lambda msg: msg.text == "Назад") + def handle_back(message: Message): + bot.send_message(message.chat.id, "Возврат в главное меню", reply_markup=get_main_menu()) diff --git a/app/bot/handlers/unsubscribe.py b/app/bot/handlers/unsubscribe.py new file mode 100644 index 0000000..89a7094 --- /dev/null +++ b/app/bot/handlers/unsubscribe.py @@ -0,0 +1,35 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton +from telebot import logger +from app.bot.constants import UserStates +from app.bot.utils.auth import auth +from app.bot.utils.helpers import get_user_subscribed_regions +from app.bot.utils.regions import format_regions_list +from app.bot.processors.unsubscribe_processor import process_unsubscribe_button + + +def register_handlers(bot, app, state_manager): + @bot.message_handler(commands=['unsubscribe']) + @bot.message_handler(func=lambda message: message.text == 'Отписаться') + def handle_unsubscribe(message): + with app.app_context(): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + if not auth(chat_id, app): + bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + else: + state_manager.set_state(chat_id, UserStates.WAITING_INPUT) + + user_subscriptions = get_user_subscribed_regions(chat_id) + formated_user_subscriptions = format_regions_list(user_subscriptions) + + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton("Отмена", callback_data="cancel_input")) + + bot.send_message(chat_id, + f"Введите номер(а) региона(ов) через запятую подписки которых вы хотите удалить:\n\n{formated_user_subscriptions}", + reply_markup=markup) + + bot.register_next_step_handler(message, process_unsubscribe_button, app, bot, chat_id, state_manager) \ No newline at end of file diff --git a/app/bot/middlewares/__init__.py b/app/bot/i18n/__ini__.py similarity index 100% rename from app/bot/middlewares/__init__.py rename to app/bot/i18n/__ini__.py diff --git a/app/bot/i18n/messages.py b/app/bot/i18n/messages.py new file mode 100644 index 0000000..fd192e9 --- /dev/null +++ b/app/bot/i18n/messages.py @@ -0,0 +1,12 @@ +translations = { + "ru": { + "greeting": "👋 Привет, {username}!", + "not_registered": "❌ Вы не зарегистрированы.", + "menu_settings": "⚙️ Настройки", + }, + "en": { + "greeting": "👋 Hello, {username}!", + "not_registered": "❌ You are not registered.", + "menu_settings": "⚙️ Settings", + } +} diff --git a/app/bot/keyboards/active_triggers.py b/app/bot/keyboards/active_triggers.py new file mode 100644 index 0000000..d1879ff --- /dev/null +++ b/app/bot/keyboards/active_triggers.py @@ -0,0 +1,39 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + +from config import REGIONS_PER_PAGE + + +def create_region_keyboard(regions, page, page_size=REGIONS_PER_PAGE): + markup = InlineKeyboardMarkup(row_width=2) + + start = page * page_size + end = start + page_size + page_regions = regions[start:end] + + for region in page_regions: + region_id_str = f"{region['id']:02d}" # 2 цифры, с ведущими нулями + button_text = f"{region['id']:02d}: {region['name']}" + button = InlineKeyboardButton(text=button_text, callback_data=f"region_{region_id_str}") + markup.add(button) + + # Пагинация + navigation_buttons = [] + if page > 0: + navigation_buttons.append(InlineKeyboardButton("⬅️", callback_data=f"regions_page_{page - 1}")) + if end < len(regions): + navigation_buttons.append(InlineKeyboardButton("➡️", callback_data=f"regions_page_{page + 1}")) + if navigation_buttons: + markup.add(*navigation_buttons) + + # Кнопка отмены в самый низ + cancel_button = InlineKeyboardButton("Отмена", callback_data="cancel_input") + markup.add(cancel_button) + + return markup + +def create_group_keyboard(groups, region_id): + markup = InlineKeyboardMarkup() + for group in groups: + markup.add(InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) + markup.add(InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", callback_data=f"all_groups_{region_id}")) + return markup diff --git a/app/bot/keyboards/settings_menu.py b/app/bot/keyboards/settings_menu.py index f84f710..c945d86 100644 --- a/app/bot/keyboards/settings_menu.py +++ b/app/bot/keyboards/settings_menu.py @@ -2,7 +2,7 @@ from telebot.types import ReplyKeyboardMarkup, KeyboardButton def get_settings_menu(): - markup = ReplyKeyboardMarkup(resize_keyboard=True) + markup = ReplyKeyboardMarkup(one_time_keyboard=True,resize_keyboard=True) markup.add(KeyboardButton("Подписаться"),KeyboardButton("Отписаться")) markup.add(KeyboardButton("Мои подписки"),KeyboardButton("Режим уведомлений")) markup.add(KeyboardButton("Назад")) diff --git a/app/bot/middlewares/user_access.py b/app/bot/middlewares/user_access.py deleted file mode 100644 index eced72b..0000000 --- a/app/bot/middlewares/user_access.py +++ /dev/null @@ -1,49 +0,0 @@ -# app/bot/middlewares/user_middleware.py -from telebot.handler_backends import BaseMiddleware -from app.models.users import Users -from app.extensions.db import db - - -class UserVerificationMiddleware(BaseMiddleware): - """ - Middleware: проверяет наличие пользователя и флаги, работает в контексте Flask-приложения - """ - - def __init__(self, bot, flask_app): - super().__init__() - self.update_types = ['message', 'callback_query'] - self.bot = bot - self.app = flask_app # Сохраняем ссылку на Flask app - - def pre_process(self, message, data): - if hasattr(message, 'chat'): - chat_id = message.chat.id - elif hasattr(message, 'message') and hasattr(message.message, 'chat'): - chat_id = message.message.chat.id - else: - return - - try: - with self.app.app_context(): - user = db.session.query(Users).filter_by(chat_id=chat_id).first() - - if user is None: - data['user_not_found'] = True - return - - if user.is_blocked: - data['user_blocked'] = True - return - - data['user'] = user - data['user_verified'] = True - - except Exception as e: - print(f"Ошибка при проверке пользователя: {e}") - - def post_process(self, message, data, exception=None): - if exception: - print(f"Exception in handler: {exception}") - elif data.get('user_verified'): - user = data.get('user') - print(f"✅ Пользователь chat_id={user.chat_id} прошёл проверку") diff --git a/app/bot/processors/active_triggers_processor.py b/app/bot/processors/active_triggers_processor.py new file mode 100644 index 0000000..676706c --- /dev/null +++ b/app/bot/processors/active_triggers_processor.py @@ -0,0 +1,56 @@ +from telebot import types +from app.bot.keyboards.main_menu import get_main_menu +from app.bot.utils.zabbix import get_region_groups, get_all_groups_for_region, fetch_filtered_triggers +from app.bot.utils.tg_formatter import format_trigger_message # ⬅️ добавлено + +def process_region_selection(bot,chat_id, region_id): + try: + groups = get_region_groups(region_id) + if not groups: + return bot.send_message(chat_id, "Нет групп хостов для этого региона.") + + markup = types.InlineKeyboardMarkup() + for group in groups: + markup.add(types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) + markup.add(types.InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", callback_data=f"all_groups_{region_id}")) + + bot.send_message(chat_id, "Выберите группу хостов:", reply_markup=markup) + except Exception as e: + bot.send_message(chat_id, f"Ошибка при получении групп: {str(e)}", reply_markup=get_main_menu()) + + +def process_group_selection(bot, chat_id, group_id): + try: + triggers = fetch_filtered_triggers(group_id) + if not triggers: + bot.send_message(chat_id, "Нет активных событий.") + else: + send_trigger_messages(chat_id, triggers) + except Exception as e: + bot.send_message(chat_id, f"Ошибка при получении событий: {str(e)}") + + +def process_all_groups_request(bot, chat_id, region_id): + try: + all_triggers = [] + groups = get_all_groups_for_region(region_id) + for group in groups: + try: + triggers = fetch_filtered_triggers(group['groupid']) + if triggers: + all_triggers.extend(triggers) + except Exception: + continue + + if all_triggers: + send_trigger_messages(chat_id, all_triggers) + else: + bot.send_message(chat_id, "Нет активных событий.") + except Exception as e: + bot.send_message(chat_id, f"Ошибка при получении данных: {str(e)}") + + +def send_trigger_messages(chat_id, triggers): + for trigger in triggers: + text = format_trigger_message(trigger) + bot.send_message(chat_id, text, parse_mode="MarkdownV2") diff --git a/app/bot/processors/my_subscriptions_processor.py b/app/bot/processors/my_subscriptions_processor.py new file mode 100644 index 0000000..78181ef --- /dev/null +++ b/app/bot/processors/my_subscriptions_processor.py @@ -0,0 +1,39 @@ +from flask import Flask +from telebot import TeleBot, logger +from telebot.types import Message +from app.bot.constants import UserStates +from app.bot.keyboards.settings_menu import get_settings_menu +from app.bot.states import UserStateManager +from app.bot.utils.auth import auth +from app.models import Regions, Subscriptions +from app.bot.utils.regions import format_regions_list + +def handle_my_subscriptions(message: Message, bot: TeleBot, app: Flask, state_manager: UserStateManager): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + if not auth(chat_id, app): + bot.send_message(chat_id, "❌Вы не авторизованы для использования этого бота.") + logger.warning(f"Неавторизованный пользователь {chat_id} @{username}") + state_manager.set_state(chat_id, UserStates.REGISTRATION) + return + + with app.app_context(): + user_regions = ( + Regions.query + .with_entities(Regions.region_id, Regions.region_name) + .join(Subscriptions, Subscriptions.region_id == Regions.region_id) + .filter( + Subscriptions.chat_id == chat_id, + Subscriptions.active == True, + Subscriptions.skip == False + ) + .order_by(Regions.region_id.asc()) + .all() + ) + + if not user_regions: + bot.send_message(chat_id, "ℹ️Вы не подписаны ни на один регион.", reply_markup=get_settings_menu()) + else: + regions_list = format_regions_list(user_regions) + bot.send_message(chat_id, f"ℹ️Ваши активные подписки:\n{regions_list}", reply_markup=get_settings_menu()) + diff --git a/app/bot/processors/subscribe_processor.py b/app/bot/processors/subscribe_processor.py new file mode 100644 index 0000000..975ebcb --- /dev/null +++ b/app/bot/processors/subscribe_processor.py @@ -0,0 +1,75 @@ +from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, MessageID +from app.bot.keyboards.settings_menu import get_settings_menu +from app.extensions.db import db +from app import Regions, Subscriptions +from app.bot.utils.tg_audit import log_user_event +from app.bot.constants import UserStates + + +def process_subscription_button(message: Message, app, bot, chat_id: int, state_manager, bot_message: MessageID): + parts = [part.strip() for part in message.text.split(',')] + if not parts or not all(part.isdigit() for part in parts): + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton(text="Отмена", callback_data="cancel_input")) + + bot.send_message(chat_id, + "❌ Неверный ввод, введите число(а) через запятую, либо нажмите отмена.", + reply_markup=markup) + + def delayed_handler(msg): + process_subscription_button(msg, app, bot, chat_id, state_manager) + + bot.register_next_step_handler(message, delayed_handler) + return + bot.delete_message(chat_id, bot_message) + region_ids = [int(part) for part in parts] + + try: + with app.app_context(): + valid_region_ids = [r.region_id for r in Regions.query.filter(Regions.active == True).all()] + + subbed_regions = [] + invalid_regions = [] + + for region_id in region_ids: + if region_id not in valid_region_ids: + invalid_regions.append(str(region_id)) + continue + + subscription = Subscriptions.query.filter_by(chat_id=chat_id, region_id=region_id).first() + + if subscription: + if not subscription.active: + subscription.active = True + db.session.add(subscription) + subbed_regions.append(str(region_id)) + else: + new_sub = Subscriptions(chat_id=chat_id, region_id=region_id, active=True) + db.session.add(new_sub) + subbed_regions.append(str(region_id)) + + db.session.commit() + + except Exception as e: + bot.send_message(chat_id, "⚠️ Произошла ошибка при обработке запроса. Попробуйте позже.") + return + + if invalid_regions: + bot.send_message(chat_id, + f"⚠️ Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") + # Можно не менять состояние, чтобы пользователь мог повторить ввод + # И снова ждём ввод: + bot.register_next_step_handler(message, process_subscription_button, bot, chat_id, state_manager) + return + + if subbed_regions: + + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + log_user_event(chat_id, app, username, f"Подписался на регионы: {', '.join(subbed_regions)}") + + # Сбрасываем состояние, чтобы не продолжать ждать ввод + state_manager.set_state(chat_id, UserStates.SETTINGS_MENU) + + # Показываем меню + bot.send_message(chat_id, f"✅ Подписка на регионы: {', '.join(subbed_regions)} оформлена.", reply_markup=get_settings_menu()) + diff --git a/app/bot/processors/unsubscribe_processor.py b/app/bot/processors/unsubscribe_processor.py new file mode 100644 index 0000000..912a967 --- /dev/null +++ b/app/bot/processors/unsubscribe_processor.py @@ -0,0 +1,65 @@ +from flask import Flask +from telebot import TeleBot, logger +from telebot.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from app.bot.keyboards.settings_menu import get_settings_menu +from app.bot.utils.helpers import get_user_subscribed_regions +from app import Subscriptions +from app.bot.utils.tg_audit import log_user_event +from app.extensions.db import db +from app.bot.states import UserStateManager +from app.bot.constants import UserStates + +def process_unsubscribe_button(message: Message, app: Flask, bot: TeleBot, chat_id: int, state_manager: UserStateManager): + unsubbed_regions = [] + invalid_regions = [] + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + + parts = [part.strip() for part in message.text.split(',')] + if not parts or not all(part.isdigit() for part in parts): + markup = InlineKeyboardMarkup() + markup.add(InlineKeyboardButton('Отмена', callback_data='cancel_input')) + + bot.send_message(chat_id, + "Неверный ввод, введите число(а) через запятую, либо нажмите отмена.", + reply_markup=markup) + + def delayed_handler(msg): + process_unsubscribe_button(msg, app, bot, chat_id, state_manager) + + bot.register_next_step_handler(message, delayed_handler) + return + + region_ids = [int(part) for part in parts] + + try: + with app.app_context(): + valid_region_ids = [int(region[0]) for region in get_user_subscribed_regions(chat_id)] + + for region_id in region_ids: + if region_id not in valid_region_ids: + invalid_regions.append(str(region_id)) + continue + + subscription = Subscriptions.query.filter_by(chat_id=chat_id, region_id=region_id).first() + if subscription: + subscription.active = False + db.session.add(subscription) + unsubbed_regions.append(str(region_id)) + + db.session.commit() + + + except Exception as e: + bot.send_message(chat_id, "⚠ Произошла ошибка при обработке запроса. Попробуйте позже.") + logger.error(f"Unexpected Error: {e}") + return + + if unsubbed_regions: + bot.send_message(chat_id, f"✅ Вы успешно отписались от регионов: {', '.join(unsubbed_regions)}") + log_user_event(chat_id, app, username, f"Отписался от регионов: {', '.join(unsubbed_regions)}") + if invalid_regions: + bot.send_message(chat_id, + f"⚠ Регионы с ID {', '.join(invalid_regions)} не найдены среди ваших подписок и не были изменены.") + + state_manager.set_state(chat_id, UserStates.SETTINGS_MENU) + bot.send_message(chat_id, "⚙ Вернулись в меню настроек.", reply_markup=get_settings_menu()) \ No newline at end of file diff --git a/app/workers/__init__.py b/app/bot/services/__init__.py similarity index 100% rename from app/workers/__init__.py rename to app/bot/services/__init__.py diff --git a/app/bot/services/mailing_service/__init__.py b/app/bot/services/mailing_service/__init__.py new file mode 100644 index 0000000..8ddd814 --- /dev/null +++ b/app/bot/services/mailing_service/__init__.py @@ -0,0 +1,22 @@ +# __init__.py +import asyncio +from threading import Thread + +from app.bot.services.mailing_service.mailing_consumer import AsyncMailingService + +def start_mailing_service(app, bot): + def run(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(_start_async_service(app, bot)) + finally: + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + + Thread(target=run, name="AsyncMailingServiceThread", daemon=True).start() + + +async def _start_async_service(app, bot): + service = AsyncMailingService(app, bot) + await service.start() \ No newline at end of file diff --git a/app/bot/services/mailing_service/composer.py b/app/bot/services/mailing_service/composer.py new file mode 100644 index 0000000..2fe4b72 --- /dev/null +++ b/app/bot/services/mailing_service/composer.py @@ -0,0 +1,51 @@ +# composer.py + +import time +from typing import Optional, Tuple +from app.bot.utils.tg_escape_chars import escape_telegram_chars # или твоя функция, если кастомная + +def compose_telegram_message(data: dict) -> Tuple[str, Optional[str]]: + """ + Формирует сообщение для Telegram и возвращает его вместе со ссылкой (если есть). + + Args: + data (dict): сообщение из очереди + + Returns: + Tuple[str, Optional[str]]: текст сообщения и ссылка для кнопки (если есть) + """ + try: + priority_map = { + 'High': '⚠️', + 'Disaster': '⛔️' + } + priority = priority_map.get(data.get('severity', ''), '') + msg = escape_telegram_chars(data.get('msg', '')) + host = escape_telegram_chars(data.get('host', '')) + ip = escape_telegram_chars(data.get('ip', '')) + severity = escape_telegram_chars(data.get('severity', '')) + status = data.get('status', '').upper() + timestamp = int(data.get('date_reception', 0)) + time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)) + + if status == "PROBLEM": + message = ( + f"{priority} {host} ({ip})\n" + f"Описание: {msg}\n" + f"Критичность: {severity}\n" + f"Время возникновения: {time_str} Мск\n" + ) + else: + message = ( + f"✅ {host} ({ip})\n" + f"Описание: {msg}\n" + f"Критичность: {severity}\n" + f"Проблема устранена!\n" + f"Время устранения: {time_str} Мск\n" + ) + + link = data.get("link") + return message, link + + except KeyError as e: + raise ValueError(f"Missing key in data: {e}") diff --git a/app/bot/services/mailing_service/db_utils.py b/app/bot/services/mailing_service/db_utils.py new file mode 100644 index 0000000..9d04a96 --- /dev/null +++ b/app/bot/services/mailing_service/db_utils.py @@ -0,0 +1,38 @@ +# db_utils.py +from app.models import Subscriptions, Users +from sqlalchemy.orm import joinedload + +def get_recipients_by_region(app, region_id: int, severity: str = "") -> list[int]: + """ + Возвращает список chat_id, подписанных на указанный регион с учётом критичности и фильтром по is_blocked. + + Args: + app: экземпляр Flask-приложения + region_id (int): номер региона + severity (str): уровень критичности события ("Disaster", "High", и т.п.) + + Returns: + list[int]: список chat_id + """ + if region_id is None: + return [] + + with app.app_context(): + # Предположим, что поле is_blocked у пользователя, а не у подписки, + # тогда нужно сделать join по пользователям: + query = ( + Subscriptions.query + .join(Users, Users.chat_id == Subscriptions.chat_id) + .filter( + Subscriptions.region_id == region_id, + Subscriptions.active == True, + Subscriptions.skip == False, + Users.is_blocked == False # исключаем заблокированных пользователей + ) + ) + + if severity != "Disaster": + query = query.filter(Subscriptions.disaster_only == False) + + subs = query.options(joinedload(Subscriptions.user)).all() + return [sub.chat_id for sub in subs] diff --git a/app/bot/services/mailing_service/mailing_consumer.py b/app/bot/services/mailing_service/mailing_consumer.py new file mode 100644 index 0000000..60b5d8b --- /dev/null +++ b/app/bot/services/mailing_service/mailing_consumer.py @@ -0,0 +1,151 @@ +import asyncio +import json +import logging + +from telebot import apihelper +from aio_pika import connect_robust, Message, DeliveryMode +from aio_pika.abc import AbstractIncomingMessage +from app.bot.services.mailing_service.parser import parse_region_id +from app.bot.services.mailing_service.composer import compose_telegram_message +from app.bot.services.mailing_service.db_utils import get_recipients_by_region +from config import RABBITMQ_URL_FULL, RABBITMQ_QUEUE, RABBITMQ_NOTIFICATIONS_QUEUE + +logger = logging.getLogger("TeleBot") +rate_limit_semaphore = asyncio.Semaphore(25) + +class AsyncMailingService: + def __init__(self, flask_app, bot): + self.flask_app = flask_app + self.bot = bot + self.loop = asyncio.get_event_loop() + + async def start(self): + await asyncio.gather( + self.consume_raw_messages(), + self.consume_notifications() + ) + + async def consume_raw_messages(self): + while True: + try: + logger.info("[MailingService] Подключение к RabbitMQ (сырые сообщения)...") + connection = await connect_robust(RABBITMQ_URL_FULL, loop=self.loop) + async with connection: + channel = await connection.channel() + await channel.set_qos(prefetch_count=10) + raw_queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True) + notifications_queue = await channel.declare_queue(RABBITMQ_NOTIFICATIONS_QUEUE, durable=True) + + logger.info("[MailingService] Ожидание сообщений из очереди...") + + async with raw_queue.iterator() as queue_iter: + async for message in queue_iter: + await self.handle_raw_message(message, channel, notifications_queue) + except Exception as e: + logger.error(f"[MailingService] Ошибка подключения или обработки: {e}") + logger.info("[MailingService] Повторное подключение через 5 секунд...") + await asyncio.sleep(5) + + async def handle_raw_message(self, message: AbstractIncomingMessage, channel, notifications_queue): + async with message.process(): + with self.flask_app.app_context(): + try: + data = json.loads(message.body.decode("utf-8")) + logger.info(f"[MailingService] Получено сообщение: {json.dumps(data, ensure_ascii=False)}") + + region_id = parse_region_id(data.get("host")) + severity = data.get("severity", "") + + # Получатели и сообщение параллельно + recipients_task = asyncio.to_thread(get_recipients_by_region, self.flask_app, region_id, severity) + message_task = asyncio.to_thread(compose_telegram_message, data) + + recipients, (final_message, link) = await asyncio.gather(recipients_task, message_task) + + logger.info(f"[MailingService] Получатели: {recipients}") + if link: + logger.info(f"[MailingService] Сообщение для Telegram: {final_message} {link}") + else: + logger.info(f"[MailingService] Сообщение для Telegram: {final_message}") + + # Формируем и публикуем индивидуальные уведомления в очередь отправки + for chat_id in recipients: + notification_payload = { + "chat_id": chat_id, + "message": final_message, + "link": link or None + } + body = json.dumps(notification_payload).encode("utf-8") + msg = Message(body, delivery_mode=DeliveryMode.PERSISTENT) + await channel.default_exchange.publish(msg, routing_key=notifications_queue.name) + + logger.info(f"[MailingService] Поставлено в очередь уведомлений: {len(recipients)} сообщений") + + except Exception as e: + logger.exception(f"[MailingService] Ошибка обработки сообщения: {e}") + + async def consume_notifications(self): + while True: + try: + logger.info("[MailingService] Подключение к RabbitMQ (уведомления для отправки)...") + connection = await connect_robust(RABBITMQ_URL_FULL, loop=self.loop) + async with connection: + channel = await connection.channel() + await channel.set_qos(prefetch_count=5) + notif_queue = await channel.declare_queue(RABBITMQ_NOTIFICATIONS_QUEUE, durable=True) + + logger.info("[MailingService] Ожидание сообщений из очереди уведомлений...") + + async with notif_queue.iterator() as queue_iter: + async for message in queue_iter: + await self.handle_notification_message(message) + except Exception as e: + logger.error(f"[MailingService] Ошибка подключения или обработки уведомлений: {e}") + logger.info("[MailingService] Повторное подключение через 5 секунд...") + await asyncio.sleep(5) + + async def handle_notification_message(self, message: AbstractIncomingMessage): + async with message.process(): + try: + data = json.loads(message.body.decode("utf-8")) + chat_id = data.get("chat_id") + message_text = data.get("message") + link = data.get("link") + if link: + message_text = f"{message_text} {link}" + # TODO: расширить логику отправки с кнопкой, если нужно + + await self.send_message(chat_id, message_text) + except Exception as e: + logger.error(f"[MailingService] Ошибка отправки уведомления: {e}") + # Можно реализовать message.nack() для повторной попытки + + async def send_message(self, chat_id, message): + telegram_id = "unknown" + try: + await rate_limit_semaphore.acquire() + + def get_telegram_id(): + with self.flask_app.app_context(): + from app.models.users import Users + user = Users.query.filter_by(chat_id=chat_id).first() + return user.telegram_id if user else "unknown" + + telegram_id = await asyncio.to_thread(get_telegram_id) + + await asyncio.to_thread(self.bot.send_message, chat_id, message, parse_mode="HTML") + + formatted_message = message.replace("\n", " ").replace("\r", "") + logger.info(f"[MailingService] Отправлено уведомление {telegram_id} ({chat_id}): {formatted_message}") + + except apihelper.ApiTelegramException as e: + if "429" in str(e): + logger.warning(f"[MailingService] Rate limit для {telegram_id} ({chat_id}), ждем и повторяем...") + await asyncio.sleep(1) + await self.send_message(chat_id, message) + else: + logger.error(f"[MailingService] Ошибка отправки сообщения {telegram_id} ({chat_id}): {e}") + except Exception as e: + logger.error(f"[MailingService] Неожиданная ошибка отправки сообщения {telegram_id} ({chat_id}): {e}") + finally: + rate_limit_semaphore.release() diff --git a/app/bot/services/mailing_service/parser.py b/app/bot/services/mailing_service/parser.py new file mode 100644 index 0000000..3be2cf6 --- /dev/null +++ b/app/bot/services/mailing_service/parser.py @@ -0,0 +1,19 @@ +# parser.py +import re + + +def parse_region_id(host: str) -> int | None: + """ + Извлекает region_id из строки host. + Формат: p<...>, например p18ecpapp01 → region_id = 18 + + Returns: + int | None: номер региона или None + """ + if not host or not host.startswith("p"): + return None + + match = re.match(r"^p(\d+)", host) + if match: + return int(match.group(1)) + return None diff --git a/app/bot/services/mailing_service/recepient_resolver.py b/app/bot/services/mailing_service/recepient_resolver.py new file mode 100644 index 0000000..716c374 --- /dev/null +++ b/app/bot/services/mailing_service/recepient_resolver.py @@ -0,0 +1,8 @@ +from .parser import parse_region_id +from .db_utils import get_recipients_by_features +from app import Systems + +async def get_recipients_from_data(data: dict, flask_app) -> list[int]: + system_names = [sys.system_name for sys in Systems.query.all()] + parsed = parse_message(data, system_names) + return get_recipients_by_features(parsed, flask_app) diff --git a/app/bot/states.py b/app/bot/states.py index e69de29..822d38d 100644 --- a/app/bot/states.py +++ b/app/bot/states.py @@ -0,0 +1,24 @@ +# app/bot/user_state_manager.py +from app.bot.constants import UserStates +from telebot import logger +class UserStateManager: + def __init__(self): + self.user_states = {} + + def set_state(self, chat_id: int, state: UserStates): + """Устанавливает состояние для пользователя.""" + if not isinstance(state, UserStates): + raise ValueError("state должен быть экземпляром UserStates Enum") + self.user_states[chat_id] = state + logger.debug(f"[StateManager] ✅ Установлено состояние для {chat_id}: {state.name}") + + def get_state(self, chat_id: int) -> UserStates: + """Получает текущее состояние пользователя. По умолчанию - MAIN_MENU.""" + state = self.user_states.get(chat_id, UserStates.MAIN_MENU) + logger.debug(f"[StateManager] 📌 Текущее состояние для {chat_id}: {state.name}") + return state + + def reset_state(self, chat_id: int): + """Сбрасывает состояние пользователя в главное меню.""" + self.user_states[chat_id] = UserStates.MAIN_MENU + logger.debug(f"[StateManager] 🔄 Сброс состояния для {chat_id}. Назначено: MAIN_MENU") diff --git a/app/bot/telezab_bot.py b/app/bot/telezab_bot.py index 18e4a3d..74d500d 100644 --- a/app/bot/telezab_bot.py +++ b/app/bot/telezab_bot.py @@ -1,24 +1,10 @@ -# app/bot/telezab_bot.py import telebot -from app.bot.config import BOT_TOKEN -from app.bot.handlers import start, main_menu, settings, help, registration -from app.bot.middlewares.user_access import UserVerificationMiddleware -from app import create_app +import logging +logger = telebot.logger # Используем логгер telebot +logger.setLevel(logging.INFO) # Уровень логов -bot = telebot.TeleBot(BOT_TOKEN, use_class_middlewares=True, parse_mode='HTML') -flask_app = create_app() - -# Регистрируем обработчики -start.register_handlers(bot) -main_menu.register_handlers(bot) -settings.register_handlers(bot) -help.register_handlers(bot) -registration.register_handlers(bot) - -# Потом подключаем middleware -user_verification_middleware = UserVerificationMiddleware(bot, flask_app) -bot.setup_middleware(user_verification_middleware) - -def run_bot(): - bot.infinity_polling() +def run_bot(app, bot): + # Перед запуском polling нужно push app_context, чтобы работал Flask + app.app_context().push() + bot.infinity_polling() \ No newline at end of file diff --git a/app/bot/utils/__init__.py b/app/bot/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bot/utils/auth.py b/app/bot/utils/auth.py new file mode 100644 index 0000000..2a7038f --- /dev/null +++ b/app/bot/utils/auth.py @@ -0,0 +1,32 @@ +from app.models.users import Users +from config import SUPPORT_EMAIL + +def check_registration(bot, message,app): + chat_id = message.chat.id + username = f"{message.from_user.username}" if message.from_user.username else "N/A" + + with app.app_context(): + user = Users.query.filter_by(chat_id=chat_id).first() + + if not user: + text = ( + f'❌ Вы не зарегистрированы.\n\n' + f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n' + f'В теме письма указать "Подтверждение регистрации в боте TeleZab".\n' + f'В теле письма указать:\n' + f'1. ФИО\n' + f'2. Ваш Chat ID: {chat_id}\n' + f'3. Ваше имя пользователя: @{username}' + ) + bot.send_message(chat_id, text, parse_mode="HTML", reply_markup=None) + return None + + return user + +def auth(chat_id,app) -> bool: + """ + Проверка, есть ли chat_id в белом списке (whitelist) + """ + with app.app_context(): + user = Users.query.filter_by(chat_id=chat_id).first() + return user is not None \ No newline at end of file diff --git a/app/bot/utils/cancel.py b/app/bot/utils/cancel.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bot/utils/helpers.py b/app/bot/utils/helpers.py new file mode 100644 index 0000000..a137cce --- /dev/null +++ b/app/bot/utils/helpers.py @@ -0,0 +1,55 @@ +from datetime import datetime, timezone + +import telebot + +from app.extensions.db import db +from app.models.users import Users +from app.models.regions import Regions +from app.models.subscriptions import Subscriptions +from app.models.userevents import UserEvents + + +def is_whitelisted(chat_id: int) -> tuple[bool, str | None]: + """Проверяет, есть ли пользователь с заданным chat_id и не заблокирован ли он.""" + user = Users.query.filter_by(chat_id=chat_id).first() + if user: + if user.is_blocked: + return False, "Ваш доступ заблокирован." + return True, None + return False, None + + +def get_sorted_regions() -> list[tuple[int, str]]: + """Возвращает список всех активных регионов, отсортированных по region_id.""" + return ( + Regions.query + .filter_by(active=True) + .with_entities(Regions.region_id, Regions.region_name) + .order_by(Regions.region_id.asc()) + .all() + ) + + +def get_user_subscribed_regions(chat_id: int) -> list[tuple[int, str]]: + """Возвращает список активных подписанных регионов пользователя.""" + regions = ( + Regions.query + .join(Subscriptions, Subscriptions.region_id == Regions.region_id) + .filter( + Subscriptions.chat_id == chat_id, + Subscriptions.active.is_(True), + Subscriptions.skip.is_(False), + Regions.active.is_(True) + ) + .with_entities(Regions.region_id, Regions.region_name) + .order_by(Regions.region_id.asc()) + .all() + ) + return regions + + +def format_regions_list(regions: list[tuple[int, str]]) -> str: + """Форматирует список регионов для отображения.""" + return '\n'.join(f"{region_id} - {region_name}" for region_id, region_name in regions) + + diff --git a/app/bot/utils/regions.py b/app/bot/utils/regions.py new file mode 100644 index 0000000..26c5aa6 --- /dev/null +++ b/app/bot/utils/regions.py @@ -0,0 +1,31 @@ +# app/bot/utils/regions.py +from typing import List, Dict + +from app.models import Regions + +def get_sorted_regions(): + """ + Получить отсортированный список регионов (из базы, например). + """ + # Здесь предполагается, что вызывающий код находится в контексте Flask (app.app_context()) + return Regions.query.filter(Regions.active == True).order_by(Regions.region_id).all() + +def get_sorted_regions_plain() -> List[Dict[str, str]]: + regions = Regions.query.filter(Regions.active == True).order_by(Regions.region_id).all() + return [{"id": r.region_id, "name": r.region_name} for r in regions] + +def format_regions_list(regions): + """ + Форматировать список регионов в удобочитаемый текст + """ + lines = [] + for region in regions: + lines.append(f"✅ {region.region_id}: {region.region_name}") + return "\n".join(lines) + +def format_regions_list_marked(regions, subscribed_region_ids): + lines = [] + for region in regions: + mark = "✅" if region.region_id in subscribed_region_ids else "❌" + lines.append(f"{mark} {region.region_id}: {region.region_name}") + return "\n".join(lines) \ No newline at end of file diff --git a/app/bot/utils/tg_audit.py b/app/bot/utils/tg_audit.py new file mode 100644 index 0000000..4661ee7 --- /dev/null +++ b/app/bot/utils/tg_audit.py @@ -0,0 +1,30 @@ +# app/bot/utils/logging.py +from datetime import datetime, timezone + +from flask import Flask + +from app.extensions.db import db +from app.models import UserEvents +from telebot import logger + +def log_user_event(chat_id: int, app: Flask, username: str, action: str) -> None: + """ + Логирует действие пользователя в базу с использованием ORM. + """ + try: + with app.app_context(): + timestamp = datetime.now(timezone.utc) + + event = UserEvents( + chat_id=chat_id, + telegram_id=username, + action=action, + timestamp=timestamp + ) + db.session.add(event) + db.session.commit() + + formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') + logger.info(f"User event logged: {chat_id} (@{username}) - {action} at {formatted_time}.") + except Exception as e: + logger.error(f"Error logging user event: {e}") diff --git a/app/bot/utils/tg_escape_chars.py b/app/bot/utils/tg_escape_chars.py new file mode 100644 index 0000000..2305101 --- /dev/null +++ b/app/bot/utils/tg_escape_chars.py @@ -0,0 +1,20 @@ +def escape_telegram_chars(text): + """ + Экранирует запрещённые символы для Telegram API: + < -> < + > -> > + & -> & + Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием. + """ + replacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', # Для кавычек + } + + # Применяем замены + for char, replacement in replacements.items(): + text = text.replace(char, replacement) + + return text \ No newline at end of file diff --git a/app/bot/utils/tg_formatter.py b/app/bot/utils/tg_formatter.py new file mode 100644 index 0000000..69d7a6e --- /dev/null +++ b/app/bot/utils/tg_formatter.py @@ -0,0 +1,35 @@ +from datetime import datetime +from pytz import timezone +from app.bot.utils.tg_escape_chars import escape_telegram_chars + +def format_trigger_message(trigger, zabbix_url: str) -> str: + tz = timezone('Europe/Moscow') + priority_map = {'4': 'HIGH', '5': 'DISASTER'} + + event_time_epoch = int(trigger.get('lastEvent', {}).get('clock', trigger.get('lastchange', 0))) + event_time = datetime.fromtimestamp(event_time_epoch, tz=tz) + event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') + + host = trigger.get('hosts', [{}])[0].get('name', 'Неизвестно') + priority = priority_map.get(str(trigger.get('priority')), 'Неизвестно') + description = escape_telegram_chars(trigger.get('description', '')).replace("{HOST.NAME}", host) + + items = trigger.get('items', []) + item_ids = [item['itemid'] for item in items] + + for i, item in enumerate(items): + placeholder = f"{{ITEM.LASTVALUE{i + 1}}}" + if placeholder in description: + description = description.replace(placeholder, item.get('lastvalue', '?')) + + batchgraph_link = f"{zabbix_url}/history.php?action=batchgraph&" + batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) + batchgraph_link += "&graphtype=0" + + return ( + f"Host: {host}\n" + f"Описание: {description}\n" + f"Критичность: {priority}\n" + f"Время создания: {event_time_formatted}\n" + f'URL: Ссылка на график' + ) diff --git a/app/bot/utils/zabbix.py b/app/bot/utils/zabbix.py new file mode 100644 index 0000000..5a88f1e --- /dev/null +++ b/app/bot/utils/zabbix.py @@ -0,0 +1,119 @@ +import time +from datetime import datetime +from pytz import timezone +from pyzabbix import ZabbixAPI, ZabbixAPIException +from telebot import logger + +from config import ZABBIX_URL, ZABBIX_API_TOKEN, ZABBIX_VERIFY_SSL, ZABBIX_TZ +from app.bot.utils.tg_escape_chars import escape_telegram_chars +TZ = timezone(ZABBIX_TZ) +verify_ssl = ZABBIX_VERIFY_SSL + +def get_region_groups(region_id: str): + """ + Получает список групп, имя которых содержит регион region_id, исключая 'test'. + """ + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + zapi.session.verify = verify_ssl + + host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) + filtered_groups = [group for group in host_groups if 'test' not in group['name'].lower()] + return filtered_groups + except Exception as e: + logger.error(f"Error getting region groups for '{region_id}': {e}") + return [] + +def get_all_groups_for_region(region_id: str): + """ + Аналогично get_region_groups, получение всех групп по региону. + """ + return get_region_groups(region_id) + +def fetch_filtered_triggers(group_id): + """ + Получение и фильтрация триггеров с severities 4 и 5, + формирование HTML сообщений для отправки в Telegram. + """ + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + zapi.session.verify = verify_ssl + + problems = zapi.problem.get( + severities=[4, 5], + suppressed=0, + acknowledged=0, + groupids=group_id + ) + trigger_ids = [problem["objectid"] for problem in problems] + + if not trigger_ids: + return [] + + triggers = zapi.trigger.get( + triggerids=trigger_ids, + output=["triggerid", "description", "priority"], + selectHosts=["hostid", "name"], + monitored=1, + expandDescription=1, + expandComment=1, + selectItems=["itemid", "lastvalue"], + selectLastEvent=["clock", "eventid"] + ) + + events = zapi.event.get( + severities=[4, 5], + objectids=trigger_ids, + select_alerts="mediatype" + ) + + pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"} + + pnet_triggers = [] + event_dict = {event["objectid"]: event for event in events} + + for trigger in triggers: + event = event_dict.get(trigger["triggerid"]) + if event: + for alert in event["alerts"]: + if alert["mediatypes"] and alert["mediatypes"][0]["name"] in pnet_mediatypes: + pnet_triggers.append(trigger) + break + + triggers_sorted = sorted(pnet_triggers, key=lambda t: int(t['lastEvent']['clock'])) + + + priority_map = {'4': 'HIGH', '5': 'DISASTER'} + trigger_messages = [] + + for trigger in triggers_sorted: + event_time_epoch = int(trigger['lastEvent']['clock']) + event_time = datetime.fromtimestamp(event_time_epoch, tz=TZ) + description = escape_telegram_chars(trigger['description']) + host = trigger['hosts'][0]['name'] + priority = priority_map.get(trigger['priority'], 'Неизвестно') + item_ids = [item['itemid'] for item in trigger['items']] + batchgraph_link = f"{ZABBIX_URL}/history.php?action=batchgraph&" + batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) + batchgraph_link += "&graphtype=0" + description = description.replace("{HOST.NAME}", host) + for i, item in enumerate(trigger['items']): + lastvalue_placeholder = f"{{ITEM.LASTVALUE{i + 1}}}" + if lastvalue_placeholder in description: + description = description.replace(lastvalue_placeholder, item['lastvalue']) + event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') + message = (f"Host: {host}\n" + f"Описание: {description}\n" + f"Критичность: {priority}\n" + f"Время создания: {event_time_formatted}\n" + f'URL: Ссылка на график') + trigger_messages.append(message) + + logger.info(f"Fetched {len(triggers_sorted)} triggers for group {group_id}.") + return trigger_messages + + except Exception as e: + logger.error(f"Error fetching triggers for group {group_id}: {e}") + return [] diff --git a/app/extensions/bot_send.py b/app/extensions/bot_send.py new file mode 100644 index 0000000..aa47016 --- /dev/null +++ b/app/extensions/bot_send.py @@ -0,0 +1,25 @@ +import json + +import requests + +from config import TOKEN + +TELEGRAM_API_URL = "https://api.telegram.org/bot{}/sendMessage".format(TOKEN) + +def bot_send_message(chat_id: int, text: str): + + keyboard = { + "keyboard": [ + [{"text": "Продолжить"}] + ], + "resize_keyboard": True, + "one_time_keyboard": True, + } + + payload = {'chat_id': chat_id, 'text': text, 'reply_markup': json.dumps(keyboard)} + + try: + response = requests.get(TELEGRAM_API_URL, json=payload) + response.raise_for_status() + except requests.exceptions.HTTPError as e: + pass diff --git a/app/extensions/db.py b/app/extensions/db.py index f758333..c999e6a 100644 --- a/app/extensions/db.py +++ b/app/extensions/db.py @@ -1,3 +1,4 @@ +#app/extensions/db.py from flask_sqlalchemy import SQLAlchemy db: SQLAlchemy = SQLAlchemy() \ No newline at end of file diff --git a/app/extensions/rabbitmq.py b/app/extensions/rabbitmq.py new file mode 100644 index 0000000..23ebbb5 --- /dev/null +++ b/app/extensions/rabbitmq.py @@ -0,0 +1,63 @@ +import pika +from config import RABBITMQ_HOST, RABBITMQ_PORT, RABBITMQ_LOGIN, RABBITMQ_PASS, RABBITMQ_QUEUE +import time + +class RabbitMQClient: + def __init__(self): + self._connect() + + def _connect(self): + credentials = pika.PlainCredentials(RABBITMQ_LOGIN, RABBITMQ_PASS) + parameters = pika.ConnectionParameters( + host=RABBITMQ_HOST, + port=RABBITMQ_PORT, + credentials=credentials, + heartbeat=600, + blocked_connection_timeout=300 + ) + self.connection = pika.BlockingConnection(parameters) + self.channel = self.connection.channel() + self.channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True) + + def publish_message(self, message: str, retry=1): + try: + if not self.connection or self.connection.is_closed or not self.channel or self.channel.is_closed: + self._connect() + self.channel.basic_publish( + exchange='', + routing_key=RABBITMQ_QUEUE, + body=message, + properties=pika.BasicProperties( + delivery_mode=2, + ) + ) + except (pika.exceptions.ChannelClosedByBroker, + pika.exceptions.ConnectionClosed, + pika.exceptions.AMQPConnectionError) as e: + if retry > 0: + # Короткая пауза перед переподключением + time.sleep(1) + self._connect() + self.publish_message(message, retry=retry-1) + else: + raise e + + def close(self): + if self.connection and self.connection.is_open: + self.connection.close() + + + + def close(self): + if self.connection and self.connection.is_open: + self.connection.close() + + +# Глобальная переменная +rabbitmq_client = None + +def get_rabbitmq_client(): + global rabbitmq_client + if rabbitmq_client is None: + rabbitmq_client = RabbitMQClient() + return rabbitmq_client \ No newline at end of file diff --git a/app/models/userevents.py b/app/models/userevents.py index 0c2807f..69621c5 100644 --- a/app/models/userevents.py +++ b/app/models/userevents.py @@ -2,6 +2,7 @@ from app.extensions.db import db class UserEvents(db.Model): + __tablename__ = "user_events" id = db.Column(db.Integer, primary_key=True) chat_id = db.Column(db.Integer, nullable=False) telegram_id = db.Column(db.String(80), nullable=False) diff --git a/app/routes/api/notifications.py b/app/routes/api/notifications.py index c9e9476..8ef410f 100644 --- a/app/routes/api/notifications.py +++ b/app/routes/api/notifications.py @@ -1,12 +1,21 @@ from flask import Blueprint, request, jsonify -from app.services.notifications_service import NotificationService - -notification_bp = Blueprint('notification', __name__,url_prefix='/notifications') +import app.extensions.rabbitmq as rabbitmq_mod +import json +notification_bp = Blueprint('notification', __name__, url_prefix='/notification') @notification_bp.route('/', methods=['POST'], strict_slashes=False) -def notification(): - service = NotificationService() +def send_notification(): data = request.get_json() - result, status = service.process_notification(data) - return jsonify(result), status + if not data: + return jsonify({"error": "Empty JSON payload"}), 400 + + + message = json.dumps(data) + + client = rabbitmq_mod.get_rabbitmq_client() + try: + client.publish_message(message) + return jsonify({"status": "message queued"}), 200 + except Exception as e: + return jsonify({"error": str(e)}), 500 diff --git a/app/services/bot/bot_database.py b/app/services/bot/bot_database.py deleted file mode 100644 index 5134cf1..0000000 --- a/app/services/bot/bot_database.py +++ /dev/null @@ -1,72 +0,0 @@ -from datetime import datetime, timezone - -from app.extensions.db import db -from app.models import Users, Regions, Subscriptions, UserEvents - -import telebot # Для логов, можно заменить на кастомный логгер - - -def is_whitelisted(chat_id): - """Проверяет, есть ли пользователь с заданным chat_id в базе данных и не заблокирован ли он.""" - try: - user = Users.query.filter_by(chat_id=chat_id).first() - if user: - if user.is_blocked: - return False, "Ваш доступ заблокирован." - return True, None - return False, None - except Exception as e: - telebot.logger.error(f"Ошибка при проверке пользователя: {e}") - return False, "Произошла ошибка при проверке доступа." - - -def get_sorted_regions(): - """Получить список активных регионов, отсортированных по region_id.""" - return ( - Regions.query - .filter_by(active=True) - .order_by(Regions.region_id.asc()) - .with_entities(Regions.region_id, Regions.region_name) - .all() - ) - - -def get_user_subscribed_regions(chat_id): - """Получить список регионов, на которые подписан пользователь.""" - return ( - Regions.query - .join(Subscriptions, Subscriptions.region_id == Regions.region_id) - .filter( - Subscriptions.chat_id == chat_id, - Subscriptions.active.is_(True), - Subscriptions.skip.is_(False) - ) - .order_by(Regions.region_id.asc()) - .with_entities(Regions.region_id, Regions.region_name) - .all() - ) - - -def format_regions_list(regions): - """Сформировать строку для отображения списка регионов.""" - return '\n'.join([f"{region_id} - {region_name}" for region_id, region_name in regions]) - - -def log_user_event(chat_id, username, action): - """Логирует действие пользователя.""" - try: - timestamp = datetime.now(timezone.utc) - event = UserEvents( - chat_id=chat_id, - telegram_id=username, - action=action, - timestamp=timestamp - ) - db.session.add(event) - db.session.commit() - - formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') - telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {formatted_time}.") - except Exception as e: - db.session.rollback() - telebot.logger.error(f"Error logging user event: {e}") diff --git a/app/services/notifications_service.py b/app/services/notifications_service.py index 02c4b5a..71171be 100644 --- a/app/services/notifications_service.py +++ b/app/services/notifications_service.py @@ -1,28 +1,28 @@ -from utilities.notification_manager import NotificationManager -from utilities.telegram_utilities import extract_region_number, format_message -from flask import current_app - - - -class NotificationService: - def __init__(self): - self.logger = current_app.logger - self.manager = NotificationManager(self.logger) - - def process_notification(self, data): - self.logger.info(f"Получены данные уведомления: {data}") - - region_id = extract_region_number(data.get("host")) - if region_id is None: - self.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}") - return {"status": "error", "message": "Invalid host format"}, 400 - - self.logger.debug(f"Извлечён номер региона: {region_id}") - - subscribers = self.manager.get_subscribers(region_id, data['severity']) - - if self.manager.is_region_active(region_id): - message = format_message(data) - self.manager.send_notifications(subscribers, message) - - return {"status": "success"}, 200 +# from utilities.notification_manager import NotificationManager +# from utilities.telegram_utilities import extract_region_number, format_message +# from flask import current_app +# +# +# +# class NotificationService: +# def __init__(self): +# self.logger = current_app.logger +# self.manager = NotificationManager(self.logger) +# +# def process_notification(self, data): +# self.logger.info(f"Получены данные уведомления: {data}") +# +# region_id = extract_region_number(data.get("host")) +# if region_id is None: +# self.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}") +# return {"status": "error", "message": "Invalid host format"}, 400 +# +# self.logger.debug(f"Извлечён номер региона: {region_id}") +# +# subscribers = self.manager.get_subscribers(region_id, data['severity']) +# +# if self.manager.is_region_active(region_id): +# message = format_message(data) +# self.manager.send_notifications(subscribers, message) +# +# return {"status": "success"}, 200 diff --git a/app/services/users_service.py b/app/services/users_service.py index 4d0af21..4254af1 100644 --- a/app/services/users_service.py +++ b/app/services/users_service.py @@ -6,9 +6,9 @@ from typing import Dict, List, Optional, Tuple, Any from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload -from sqlalchemy import or_ from app import db +from app.extensions.bot_send import bot_send_message from app.models import Users # Предполагаем, что app.models/__init__.py экспортирует Users from app.extensions.audit_logger import AuditLogger @@ -188,6 +188,10 @@ def add_user(user_data: Dict[str, Any], actor_user: Any) -> Tuple[Dict[str, str] auditlog.users(action_type="add", actor_display_name=actor_user.display_name, ldap_user_id=actor_user.id, affected_chat_id=chat_id, telegram_id=telegram_id, email=user_email) + try: + bot_send_message(chat_id,"Регистрация пройдена успешно. нажмите продолжить что бы начать пользоваться ботом!") + except Exception as e: + logger.info(f"Ошибка при отправке сообщения {chat_id}: {e}") return {'message': 'Пользователь добавлен успешно'}, 201 except IntegrityError as e: db.session.rollback() diff --git a/app/workers/rabbitmq_worker.py b/app/workers/rabbitmq_worker.py deleted file mode 100644 index 22ac96d..0000000 --- a/app/workers/rabbitmq_worker.py +++ /dev/null @@ -1,57 +0,0 @@ -import asyncio -import json -import logging -from aio_pika import connect_robust, exceptions as aio_exceptions -from app import create_app, db -from app.models import Users - -logger = logging.getLogger(__name__) -rate_limit_semaphore = asyncio.Semaphore(25) - -RABBITMQ_URL = "amqp://guest:guest@localhost/" -RABBITMQ_QUEUE = "your_queue" - -async def send_message(backend_bot, chat_id, message_text): - telegram_id = "unknown" - - try: - async with rate_limit_semaphore: - async def get_user(): - with app.app_context(): - user = Users.query.get(chat_id) - return user.telegram_id if user else "unknown" - - telegram_id = await asyncio.to_thread(get_user) - await asyncio.to_thread( - backend_bot.bot.send_message, - chat_id, - message_text, - parse_mode="HTML" - ) - logger.info(f"[RabbitMQ] Sent to {telegram_id} ({chat_id}): {message_text}") - - except Exception as e: - logger.error(f"Error sending message to {telegram_id} ({chat_id}): {e}") - -async def consume_from_queue(backend_bot): - while True: - try: - connection = await connect_robust(RABBITMQ_URL) - async with connection: - channel = await connection.channel() - queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True) - - async for message in queue: - async with message.process(): - try: - data = json.loads(message.body.decode('utf-8')) - await send_message(backend_bot, data["chat_id"], data["message"]) - except (json.JSONDecodeError, KeyError) as e: - logger.error(f"Error decoding message: {e}") - - except aio_exceptions.AMQPError as e: - logger.error(f"RabbitMQ AMQPError: {e}") - except Exception as e: - logger.error(f"Unhandled error in consumer: {e}") - finally: - await asyncio.sleep(5) diff --git a/backend_bot.py b/backend_bot.py deleted file mode 100644 index ad35c1c..0000000 --- a/backend_bot.py +++ /dev/null @@ -1,198 +0,0 @@ -import telebot - -import telezab -from app import app -from backend_locks import bot -from bot_database import is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \ - get_user_subscribed_regions -from app.models import Regions, Subscriptions -from app.extensions.db import db -from utilities.telegram_utilities import show_main_menu, show_settings_menu -from handlers import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button - - -def handle_main_menu(message, chat_id, text): - """Обработка команд в главном меню.""" - if text == 'Регистрация': - telezab.state.set_state(chat_id, "REGISTRATION") - telezab.handle_register(message) - elif text == 'Настройки': - telezab.state.set_state(chat_id, "SETTINGS_MENU") - telezab.show_settings_menu(chat_id) - elif text == 'Помощь': - telezab.handle_help(message) - elif text == 'Активные события': - telezab.handle_active_triggers(message) - else: - bot.send_message(chat_id, "Команда не распознана.") - show_main_menu(chat_id) - - -def handle_settings_menu(message, chat_id, text): - """Обработка команд в меню настроек.""" - if text.lower() == 'подписаться': - telezab.state.set_state(chat_id, "SUBSCRIBE") - handle_subscribe_button(message) - elif text.lower() == 'отписаться': - telezab.state.set_state(chat_id, "UNSUBSCRIBE") - handle_unsubscribe_button(message) - elif text.lower() == 'мои подписки': - handle_my_subscriptions_button(message) - elif text.lower() == 'активные регионы': - handle_active_regions_button(message) - elif text.lower() == "режим уведомлений": - handle_notification_mode_button(message) - elif text.lower() == 'назад': - telezab.state.set_state(chat_id, "MAIN_MENU") - show_main_menu(chat_id) - else: - bot.send_message(chat_id, "Команда не распознана.") - show_settings_menu(chat_id) - - -def handle_subscribe_button(message): - chat_id = message.chat.id - if not is_whitelisted(chat_id): - bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") - return - username = message.from_user.username - if username: - username = f"@{username}" - else: - username = "N/A" - regions_list = format_regions_list(get_sorted_regions()) - - markup = telebot.types.InlineKeyboardMarkup() - markup.add(telebot.types.InlineKeyboardButton(text="Отмена", - callback_data=f"cancel_action")) - bot.send_message(chat_id, f"Отправьте номера регионов через запятую:\n{regions_list}\n", reply_markup=markup) - bot.register_next_step_handler_by_chat_id(chat_id, process_subscription_button, chat_id, username) - - -def process_subscription_button(message, chat_id, username): - subbed_regions = [] - invalid_regions = [] - if message.text.lower() == 'отмена': - bot.send_message(chat_id, "Действие отменено.") - telezab.state.set_state(chat_id, "SETTINGS_MENU") - return show_settings_menu(chat_id) - if not all(part.strip().isdigit() for part in message.text.split(',')): - markup = telebot.types.InlineKeyboardMarkup() - markup.add(telebot.types.InlineKeyboardButton(text="Отмена", - callback_data=f"cancel_action")) - bot.send_message(chat_id, "Неверный формат данных. Введите номер или номера регионов через запятую.", - reply_markup=markup) - bot.register_next_step_handler_by_chat_id(chat_id, process_subscription_button, chat_id, username) - return - region_ids = [int(part.strip()) for part in message.text.split(',')] - - with app.app_context(): - # Получаем список валидных ID регионов из базы - valid_region_ids = [r.region_id for r in Regions.query.filter(Regions.active == True).all()] - - for region_id in region_ids: - if region_id not in valid_region_ids: - invalid_regions.append(str(region_id)) - continue - - subscription = Subscriptions.query.filter_by(chat_id=chat_id, region_id=region_id).first() - if subscription: - if not subscription.active: - subscription.active = True - db.session.add(subscription) - subbed_regions.append(str(region_id)) - else: - # Уже подписан, можно тоже добавить для отчета - subbed_regions.append(str(region_id)) - else: - new_sub = Subscriptions(chat_id=chat_id, region_id=region_id, active=True) - db.session.add(new_sub) - subbed_regions.append(str(region_id)) - - db.session.commit() - - if invalid_regions: - bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") - - if subbed_regions: - bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.") - log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}") - - telezab.state.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - -def handle_unsubscribe_button(message): - chat_id = message.chat.id - if not is_whitelisted(chat_id): - bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") - telebot.logger.info(f"Unauthorized access attempt by {chat_id}") - telezab.state.set_state(chat_id, "REGISTRATION") - return show_main_menu(chat_id) - username = message.from_user.username - if username: - username = f"@{username}" - else: - username = "N/A" - # Получаем список подписок пользователя - user_regions = get_user_subscribed_regions(chat_id) - - if not user_regions: - bot.send_message(chat_id, "Вы не подписаны ни на один регион.") - telezab.state.set_state(chat_id, "SETTINGS_MENU") - return show_settings_menu(chat_id) - regions_list = format_regions_list(user_regions) - markup = telebot.types.InlineKeyboardMarkup() - markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_action")) - bot.send_message(chat_id, - f"Отправьте номер или номера регионов, от которых хотите отписаться (через запятую):\n{regions_list}\n", - reply_markup=markup) - bot.register_next_step_handler_by_chat_id(chat_id, process_unsubscription_button, chat_id, username) - - -def process_unsubscription_button(message, chat_id, username): - unsubbed_regions = [] - invalid_regions = [] - - markup = telebot.types.InlineKeyboardMarkup() - markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data="cancel_action")) - - if message.text.lower() == 'отмена': - bot.send_message(chat_id, "Действие отменено.") - telezab.state.set_state(chat_id, "SETTINGS_MENU") - return show_settings_menu(chat_id) - - # Проверка корректности формата ввода - if not all(part.strip().isdigit() for part in message.text.split(',')): - bot.send_message(chat_id, "Некорректный формат. Введите номера регионов через запятую.", reply_markup=markup) - bot.register_next_step_handler_by_chat_id(chat_id, process_unsubscription_button, chat_id, username) - return - - region_ids = [region_id.strip() for region_id in message.text.split(',')] - - with app.app_context(): - valid_region_ids = [str(region[0]) for region in get_user_subscribed_regions(chat_id)] # get_user_subscribed_regions уже внутри app_context - - for region_id in region_ids: - if region_id not in valid_region_ids: - invalid_regions.append(region_id) - continue - - subscription = db.session.query(Subscriptions).filter_by( - chat_id=chat_id, - region_id=int(region_id) - ).first() - - if subscription: - subscription.active = False - unsubbed_regions.append(region_id) - - db.session.commit() - - if invalid_regions: - bot.send_message(chat_id, f"Регион(ы) с ID {', '.join(invalid_regions)} не найдены в ваших подписках.") - - bot.send_message(chat_id, f"Отписка от регионов: {', '.join(unsubbed_regions)} выполнена.") - log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(unsubbed_regions)}") - telezab.state.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) diff --git a/backend_locks.py b/backend_locks.py deleted file mode 100644 index 4c72901..0000000 --- a/backend_locks.py +++ /dev/null @@ -1,4 +0,0 @@ -import telebot -from config import TOKEN - -bot = telebot.TeleBot(TOKEN) diff --git a/backend_zabbix.py b/backend_zabbix.py deleted file mode 100644 index d76fd92..0000000 --- a/backend_zabbix.py +++ /dev/null @@ -1,166 +0,0 @@ -import logging -import os -import re -import time -from datetime import datetime -from pytz import timezone -from pyzabbix import ZabbixAPI, ZabbixAPIException - -import backend_bot -from config import ZABBIX_URL, ZABBIX_API_TOKEN -from utilities.telegram_utilities import show_main_menu, escape_telegram_chars -verify_ssl = os.getenv("ZAPPI_IGNORE_SSL_VERIFY", "True").lower() not in ("false", "0", "no") - -zabbix_logger = logging.getLogger("pyzabbix") - - -def get_triggers_for_group(chat_id, group_id): - try: - triggers = get_zabbix_triggers(group_id) - if not triggers: - backend_bot.bot.send_message(chat_id, "Нет активных событий.") - else: - send_triggers_to_user(triggers, chat_id) - zabbix_logger.debug(f"Sent {len(triggers)} triggers to user {chat_id} for group {group_id}.") - except ZabbixAPIException as e: - zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}") - backend_bot.bot.send_message(chat_id, "Ошибка Zabbix API.") - except Exception as e: - zabbix_logger.error(f"Error getting triggers for group {group_id}: {e}") - backend_bot.bot.send_message(chat_id, "Ошибка при получении событий.") - - -def get_triggers_for_all_groups(chat_id, region_id): - try: - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - zapi.session.verify = verify_ssl - - host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) - filtered_groups = [group for group in host_groups if 'test' not in group['name'].lower()] - - all_triggers = [] - for group in filtered_groups: - try: - triggers = get_zabbix_triggers(group['groupid']) - if triggers: - all_triggers.extend(triggers) - except ZabbixAPIException as e: - zabbix_logger.error(f"Zabbix API error for group {group['groupid']} ({group['name']}): {e}") - backend_bot.bot.send_message(chat_id, f"Ошибка Zabbix API при получении событий для группы {group['name']}.") - except Exception as e: - zabbix_logger.error(f"Error getting triggers for group {group['groupid']} ({group['name']}): {e}") - backend_bot.bot.send_message(chat_id, f"Ошибка при получении событий для группы {group['name']}.") - - if all_triggers: - send_triggers_to_user(all_triggers, chat_id) - zabbix_logger.debug(f"Sent {len(all_triggers)} triggers to user {chat_id} for region {region_id}.") - else: - backend_bot.bot.send_message(chat_id, "Нет активных событий.") - zabbix_logger.debug(f"No active triggers found for region {region_id}.") - show_main_menu(chat_id) - except ZabbixAPIException as e: - zabbix_logger.error(f"Zabbix API error for region {region_id}: {e}") - backend_bot.bot.send_message(chat_id, "Ошибка Zabbix API.") - show_main_menu(chat_id) - except Exception as e: - zabbix_logger.error(f"Error getting triggers for region {region_id}: {e}") - backend_bot.bot.send_message(chat_id, "Ошибка при получении событий.") - show_main_menu(chat_id) - - -def send_triggers_to_user(triggers, chat_id): - for trigger in triggers: - backend_bot.bot.send_message(chat_id, trigger, parse_mode="html") - time.sleep(1 / 5) - - -def extract_host_from_name(name): - match = re.match(r"^(.*?)\s*->", name) - return match.group(1) if match else "Неизвестный хост" - - -def get_zabbix_triggers(group_id): - pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"} - start_time = time.time() - try: - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - zapi.session.verify = verify_ssl - problems = zapi.problem.get( - severities=[4, 5], - suppressed=0, - acknowledged=0, - groupids=group_id - ) - trigger_ids = [problem["objectid"] for problem in problems] - - triggers = zapi.trigger.get( - triggerids=trigger_ids, - output=["triggerid", "description", "priority"], - selectHosts=["hostid", "name"], - monitored=1, - expandDescription=1, - expandComment=1, - selectItems=["itemid", "lastvalue"], - selectLastEvent=["clock", "eventid"] - ) - - events = zapi.event.get( - severities=[4, 5], - objectids=trigger_ids, - select_alerts="mediatype" - ) - - pnet_triggers = [] - event_dict = {event["objectid"]: event for event in events} - - for trigger in triggers: - event = event_dict.get(trigger["triggerid"]) - if event: - for alert in event["alerts"]: - if alert["mediatypes"] and alert["mediatypes"][0]["name"] in pnet_mediatypes and trigger not in pnet_triggers: - pnet_triggers.append(trigger) - break - - triggers_sorted = sorted(pnet_triggers, key=lambda t: int(t['lastEvent']['clock'])) - - zabbix_logger.info(f"Found {len(triggers_sorted)} triggers for group {group_id}.") - - moskva_tz = timezone('Europe/Moscow') - priority_map = {'4': 'HIGH', '5': 'DISASTER'} - trigger_messages = [] - - for trigger in triggers_sorted: - event_time_epoch = int(trigger['lastEvent']['clock']) - event_time = datetime.fromtimestamp(event_time_epoch, tz=moskva_tz) - description = escape_telegram_chars(trigger['description']) - host = trigger['hosts'][0]['name'] - priority = priority_map.get(trigger['priority'], 'Неизвестно') - item_ids = [item['itemid'] for item in trigger['items']] - batchgraph_link = f"{ZABBIX_URL}/history.php?action=batchgraph&" - batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) - batchgraph_link += "&graphtype=0" - description = description.replace("{HOST.NAME}", host) - for i, item in enumerate(trigger['items']): - lastvalue_placeholder = f"{{ITEM.LASTVALUE{i + 1}}}" - if lastvalue_placeholder in description: - description = description.replace(lastvalue_placeholder, item['lastvalue']) - event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') - message = (f"Host: {host}\n" - f"Описание: {description}\n" - f"Критичность: {priority}\n" - f"Время создания: {event_time_formatted}\n" - f'URL: Ссылка на график') - trigger_messages.append(message) - - end_time = time.time() - execution_time = end_time - start_time - zabbix_logger.info(f"Fetched {len(triggers_sorted)} triggers for group {group_id} in {execution_time:.2f} seconds.") - return trigger_messages - except ZabbixAPIException as e: - zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}") - return None - except Exception as e: - zabbix_logger.error(f"Error fetching triggers for group {group_id}: {e}") - return None \ No newline at end of file diff --git a/bot_database.py b/bot_database.py deleted file mode 100644 index bf7b59c..0000000 --- a/bot_database.py +++ /dev/null @@ -1,80 +0,0 @@ -from datetime import datetime, timezone -from threading import Lock - -import telebot - -from app import app -from app.models import UserEvents, Regions, Subscriptions -from app.models import Users -from app.extensions.db import db - -# Lock for database operations -db_lock = Lock() - - - -def is_whitelisted(chat_id): - """Проверяет, есть ли пользователь с заданным chat_id в базе данных и не заблокирован ли он.""" - try: - with app.app_context(): # Создаем контекст приложения - user = db.session.query(Users).filter_by(chat_id=chat_id).first() - if user: - if user.is_blocked: - return False, "Ваш доступ заблокирован." - return True, None - return False, None - except Exception as e: - telebot.logger.error(f"Ошибка при проверке пользователя: {e}") - return False, "Произошла ошибка при проверке доступа." - -def get_sorted_regions(): - with app.app_context(): - regions = ( - db.session.query(Regions.region_id, Regions.region_name) - .filter(Regions.active == True) - .order_by(Regions.region_id.asc()) - .all() - ) - return regions - - -def get_user_subscribed_regions(chat_id): - with app.app_context(): # если вызывается вне контекста Flask - results = ( - db.session.query(Regions.region_id, Regions.region_name) - .join(Subscriptions, Subscriptions.region_id == Regions.region_id) - .filter( - Subscriptions.chat_id == chat_id, - Subscriptions.active == True, - Subscriptions.skip == False - ) - .order_by(Regions.region_id.asc()) - .all() - ) - - # results — это список кортежей (region_id, region_name) - return results - - -def format_regions_list(regions): - return '\n'.join([f"{region_id} - {region_name}" for region_id, region_name in regions]) - - -def log_user_event(chat_id, username, action): - """Логирует действие пользователя с использованием ORM.""" - try: - with app.app_context(): # Создаем контекст приложения - timestamp = datetime.now(timezone.utc) # Оставляем объект datetime для БД - formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') # Форматируем для логов - - event = UserEvents( - chat_id=chat_id, - telegram_id=username, - action=action, - timestamp=timestamp # В БД передаем объект datetime - ) - db.session.add(event) - db.session.commit() - telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {formatted_time}.") - except Exception as e: - telebot.logger.error(f"Error logging user event: {e}") \ No newline at end of file diff --git a/config.py b/config.py index fa77013..ac73c1f 100644 --- a/config.py +++ b/config.py @@ -1,22 +1,41 @@ import os from datetime import timedelta +from urllib.parse import quote_plus -DEV = os.getenv('DEV') +#Настройки телеграм TOKEN = os.getenv('TELEGRAM_TOKEN') +ADMINS = os.getenv('TELEGRAM_ADMINS', '') +ADMINS_LIST = [int(admin_id.strip()) for admin_id in ADMINS.split(',') if admin_id.strip().isdigit()] +REGIONS_PER_PAGE = os.getenv('REGIONS_PER_PAGE', 10) +#Настройки Zabbix ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') ZABBIX_URL = os.getenv('ZABBIX_URL') -DB_PATH = 'db/telezab.db' +ZABBIX_VERIFY_SSL = os.getenv('ZABBIX_VERIFY_SSL', True) +ZABBIX_TZ = os.getenv('ZABBIX_TZ', 'Europe/Moscow') +#Настройки Flask и Telegram bot basedir = os.path.abspath(os.path.dirname(__file__)) DB_ABS_PATH = os.path.join(basedir, 'db/telezab.db') -SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" -BASE_URL = '/telezab' -RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') -RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') -RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') -RABBITMQ_QUEUE = 'telegram_notifications' -RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" +SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}' +SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL", "shiftsupport-rtmis@rtmis.ru") +HELP_URL = os.getenv("HELP_URL", "https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183") +#Настройки RabbitMQ +RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost') +RABBITMQ_PORT = int(os.environ.get("RABBITMQ_PORT", "5672")) +RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN', 'admin') +RABBITMQ_PASS = os.getenv('RABBITMQ_PASS', 'admin') +RABBITMQ_QUEUE = os.environ.get("RABBITMQ_QUEUE", "telegram_notifications") +RABBITMQ_NOTIFICATIONS_QUEUE = os.environ.get("RABBITMQ_NOTIFICATIONS_QUEUE", "notifications_queue") +RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST", "/") +RABBITMQ_VHOST_ENCODED = quote_plus(RABBITMQ_VHOST) -# Настройки LDAP +RABBITMQ_URL_FULL = ( + f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST_ENCODED}" +) +# Mailing settings +MAILING_MAX_WORKERS = int(os.environ.get("MAILING_MAX_WORKERS", "16")) +MAILING_RATE_LIMIT = int(os.environ.get("MAILING_RATE_LIMIT", "25")) + +# Настройки Flask-LDAP3-login LDAP_HOST = os.getenv('LDAP_HOST', 'localhost') LDAP_PORT = int(os.getenv('LDAP_PORT', 389)) LDAP_USE_SSL = os.getenv('LDAP_USE_SSL', 'False').lower() == 'true' @@ -29,15 +48,25 @@ LDAP_USER_RDN_ATTR = os.getenv('LDAP_USER_RDN_ATTR', 'sAMAccountName') LDAP_USER_LOGIN_ATTR = os.getenv('LDAP_USER_LOGIN_ATTR', 'sAMAccountName') LDAP_USER_SEARCH_SCOPE = os.getenv('LDAP_USER_SEARCH_SCOPE', 'SUBTREE') LDAP_SCHEMA = os.getenv('LDAP_SCHEMA', 'active_directory') -TZ = os.getenv('TZ', 'Europe/Moscow') -SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key' -SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}' -SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', True) -SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY',True) -SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE','Lax') -PERMANENT_SESSION_LIFETIME = timedelta(seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600))) -SESSION_REFRESH_EACH_REQUEST = os.getenv('SESSION_REFRESH_EACH_REQUEST',False) -SESSION_COOKIE_MAX_AGE = os.getenv('SESSION_COOKIE_MAX_AGE',3600) +class Config: + basedir = os.path.abspath(os.path.dirname(__file__)) + + # SQLAlchemy + SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(basedir, 'db/telezab.db')}" + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Flask session + SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key') + SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', 'True').lower() == 'true' + SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'True').lower() == 'true' + SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') + PERMANENT_SESSION_LIFETIME = timedelta( + seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600)) + ) + SESSION_COOKIE_MAX_AGE = int(os.getenv('SESSION_COOKIE_MAX_AGE', 3600)) + + # Дополнительное (если используется) + TIMEZONE = os.getenv('TZ', 'Europe/Moscow') diff --git a/handlers.py b/handlers.py deleted file mode 100644 index b8a8a6f..0000000 --- a/handlers.py +++ /dev/null @@ -1,71 +0,0 @@ -import telebot -from telebot import types - -import backend_bot -import bot_database -from utilities.telegram_utilities import show_settings_menu - - -def handle_my_subscriptions_button(message): - chat_id = message.chat.id - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - if not bot_database.is_whitelisted(chat_id): - backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") - telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}") - return - - user_regions = bot_database.get_user_subscribed_regions(chat_id) - if not user_regions: - backend_bot.bot.send_message(chat_id, "Вы не подписаны ни на один регион.") - telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}") - else: - user_regions.sort(key=lambda x: int(x[0])) # Сортировка по числовому значению region_id - regions_list = bot_database.format_regions_list(user_regions) - backend_bot.bot.send_message(chat_id, f"Ваши активные подписки:\n{regions_list}") - telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}") - show_settings_menu(chat_id) - - -def handle_active_regions_button(message): - chat_id = message.chat.id - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - if not bot_database.is_whitelisted(chat_id): - backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") - telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}") - return - - regions = bot_database.get_sorted_regions() # Используем функцию для получения отсортированных регионов - if not regions: - backend_bot.bot.send_message(chat_id, "Нет активных регионов.") - else: - regions_list = bot_database.format_regions_list(regions) - backend_bot.bot.send_message(chat_id, f"Активные регионы:\n{regions_list}") - show_settings_menu(chat_id) - - -def handle_notification_mode_button(message): - chat_id = message.chat.id - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - - telebot.logger.debug(f"Handling notification mode button for user {username} ({chat_id}).") - - if not bot_database.is_whitelisted(chat_id): - backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") - telebot.logger.warning(f"Unauthorized access attempt by {username} ({chat_id})") - return - - # Логируем успешное авторизованное использование бота - telebot.logger.info(f"User {username} ({chat_id}) is authorized and is selecting a notification mode.") - - # Отправляем клавиатуру выбора режима уведомлений - markup = types.InlineKeyboardMarkup() - markup.add(types.InlineKeyboardButton(text="Критические события", callback_data="notification_mode_disaster")) - markup.add(types.InlineKeyboardButton(text="Все события", callback_data="notification_mode_all")) - - backend_bot.bot.send_message(chat_id, - "Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n" - '1. Критические события (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n' - '2. Все события (По умолчанию) - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.', - reply_markup=markup, parse_mode="HTML") - - telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).") diff --git a/requirements.txt b/requirements.txt index 6da10f7..76542ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,11 @@ pyzabbix~=1.3.1 SQLAlchemy~=2.0.40 Flask~=3.1.0 Flask-Login~=0.6.3 +Flask-SQLAlchemy~=3.1.1 +Flask-ldap3-login~=1.0.2 Werkzeug~=3.1.3 aio-pika~=9.5.5 pika~=1.3.2 pytz~=2025.2 -concurrent-log-handler~=0.9.26 \ No newline at end of file +requests~=2.32.3 +gunicorn~=23.0.0 \ No newline at end of file diff --git a/run_flask.py b/run_flask.py new file mode 100644 index 0000000..cb0a73a --- /dev/null +++ b/run_flask.py @@ -0,0 +1,7 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + # Для локального запуска через python run_flask.py + app.run(host="0.0.0.0", port=5000, debug=True) \ No newline at end of file diff --git a/run_telegram.py b/run_telegram.py new file mode 100644 index 0000000..5631abb --- /dev/null +++ b/run_telegram.py @@ -0,0 +1,19 @@ +from app import create_app +from app.bot.services.mailing_service import start_mailing_service +from app.bot.telezab_bot import run_bot +import telebot +from config import TOKEN +from app.bot.handlers import register_handlers, register_callbacks + +if __name__ == '__main__': + app = create_app() + bot = telebot.TeleBot(TOKEN, use_class_middlewares=True, parse_mode='HTML') + + register_handlers(bot, app) + register_callbacks(bot, app) + + # Запускаем рассылку, передавая bot и app + start_mailing_service(app, bot) + + # Запускаем самого бота + run_bot(app, bot) diff --git a/supervisord.conf b/supervisord.conf deleted file mode 100644 index e0d6929..0000000 --- a/supervisord.conf +++ /dev/null @@ -1,23 +0,0 @@ -[supervisord] -nodaemon=true - -[program:flask] -command=gunicorn -w 4 -b 0.0.0.0:5000 telezab:app -directory=/app -autostart=true -autorestart=true -stderr_logfile=/app/logs/supervisord_flask.err.log -stdout_logfile=/app/logs/supervisord_flask.out.log -environment=FLASK_ENV=production -user=root -group=root - -[program:telezab] -command=python /app/telezab.py -directory=/app -autostart=true -autorestart=true -stderr_logfile=/app/logs/supervisord_telezab.err.log -stdout_logfile=/app/logs/supervisord_telezab.out.log -user=root -group=root \ No newline at end of file diff --git a/telezab.py b/telezab.py deleted file mode 100644 index 49c901e..0000000 --- a/telezab.py +++ /dev/null @@ -1,310 +0,0 @@ -import logging -from multiprocessing import Process -import telebot -from pyzabbix import ZabbixAPI -from telebot import types -import backend_bot -import bot_database -from app import app, create_app -from app.bot.telezab_bot import run_bot -from backend_locks import bot -from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups -from config import * -from app.models import Subscriptions -from app.extensions.db import db -from utilities.log_manager import LogManager -# from utilities.rabbitmq import consume_from_queue -from utilities.telegram_utilities import show_main_menu, show_settings_menu -from utilities.user_state_manager import UserStateManager - - -# Инициализируем класс UserStateManager -state = UserStateManager() - -# Инициализация LogManager -log_manager = LogManager() - -# Настройка pyTelegramBotAPI logger -telebot.logger = logging.getLogger('telebot') - -# Важно: вызов schedule_log_rotation для планировки ротации и архивации логов -# log_manager.schedule_log_rotation() - -# Handle /help command to provide instructions -@bot.message_handler(commands=['help']) -def handle_help(message): - chat_id = message.chat.id - if not bot_database.is_whitelisted(chat_id)[0]: - backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") - return - help_text = ( - '/start - Показать меню бота\n' - 'Настройки - Перейти в режим настроек и управления подписками\n' - 'Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n' - 'Помощь - Описание всех возможностей бота' - ) - backend_bot.bot.send_message(message.chat.id, help_text, parse_mode="html") - show_main_menu(message.chat.id) - -# Handle /register command for new user registration -def handle_register(message): - chat_id = message.chat.id - username = message.from_user.username - if username: - username = f"@{username}" - else: - username = "N/A" - text = ( - f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n' - f'В теме письма указать "Подтверждение регистрации в телеграм-боте TeleZab".\n' - f'В теле письма указать:\n' - f'1. ФИО\n' - f'2. Ваш Chat ID: {chat_id}\n' - f'3. Ваше имя пользователя: {username}') - backend_bot.bot.send_message(chat_id, text, parse_mode="HTML") - bot_database.log_user_event(chat_id, username, "Requested registration") - - - -@bot.callback_query_handler(func=lambda call: call.data == "cancel_action") -def handle_cancel_action(call): - chat_id = call.message.chat.id - message_id = call.message.message_id - backend_bot.bot.clear_step_handler_by_chat_id(chat_id) - backend_bot.bot.send_message(chat_id, f"Действие отменено") - backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) - state.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - return - - -@bot.callback_query_handler(func=lambda call: call.data == "cancel_active_triggers") -def handle_cancel_active_triggers(call): - chat_id = call.message.chat.id - message_id = call.message.message_id - backend_bot.bot.clear_step_handler_by_chat_id(chat_id) - backend_bot.bot.send_message(chat_id, f"Действие отменено") - backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) - state.set_state(chat_id, "MAIN_MENU") - show_main_menu(chat_id) - return - - -@bot.callback_query_handler(func=lambda call: call.data.startswith("notification_mode_")) -def handle_notification_mode_selection(call): - chat_id = call.message.chat.id - message_id = call.message.message_id - mode = call.data.split("_")[2] - username = f"@{call.from_user.username}" if call.from_user.username else "N/A" # Получаем username - - telebot.logger.debug(f"User ({chat_id}) selected notification mode: {mode}.") - - # Убираем клавиатуру - backend_bot.bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) - # telebot.logger.debug(f"Removed inline keyboard for user ({chat_id}).") - - # Обновляем режим уведомлений - disaster_only = True if mode == "disaster" else False - - try: - telebot.logger.debug(f"Attempting to update notification mode in the database for user {username} {chat_id}.") - with app.app_context(): # Создаем контекст приложения - subscriptions = db.session.query(Subscriptions).filter_by(chat_id=chat_id).all() - for subscription in subscriptions: - subscription.disaster_only = disaster_only - db.session.commit() - - mode_text = "Критические события" if disaster_only else "Все события" - backend_bot.bot.send_message(chat_id, f"Режим уведомлений успешно изменён на: {mode_text}") - telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}") - - # Логируем изменение состояния пользователя - state.set_state(chat_id, "SETTINGS_MENU") - telebot.logger.debug(f"User state for {chat_id} set to SETTINGS_MENU.") - - # Показываем меню настроек - show_settings_menu(chat_id) - telebot.logger.debug(f"Displayed settings menu to {chat_id}.") - - # Логируем событие в базу данных - bot_database.log_user_event(chat_id, username, f"Notification mode updated to: {mode_text}") - - except Exception as e: - telebot.logger.error(f"Error updating notification mode for {chat_id}: {e}") - backend_bot.bot.send_message(chat_id, "Произошла ошибка при изменении режима уведомлений.") - - # Логируем успешный ответ callback-запроса - bot.answer_callback_query(call.id) - telebot.logger.debug(f"Callback query for user ({chat_id}) answered.") - - -# Фаза 1: Запрос активных событий и выбор региона с постраничным переключением -def handle_active_triggers(message): - chat_id = message.chat.id - regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов - - start_index = 0 - markup = create_region_keyboard(regions, start_index) - backend_bot.bot.send_message(chat_id, "Выберите регион для получения активных событий:", reply_markup=markup) - - -def create_region_keyboard(regions, start_index, regions_per_page=10): - markup = types.InlineKeyboardMarkup() - end_index = min(start_index + regions_per_page, len(regions)) - - for i in range(start_index, end_index): - region_id, region_name = regions[i] - - # Форматируем region_id: добавляем ведущий 0 только если < 10 - if 0 <= int(region_id) < 10: - region_id_str = f"0{region_id}" - else: - region_id_str = str(region_id) - - button = types.InlineKeyboardButton( - text=f"{region_id_str}: {region_name}", - callback_data=f"region_{region_id_str}" - ) - markup.add(button) - - # Кнопки навигации - navigation_row = [] - if start_index > 0: - navigation_row.append(types.InlineKeyboardButton(text="<", callback_data=f"prev_{start_index}")) - if end_index < len(regions): - navigation_row.append(types.InlineKeyboardButton(text=">", callback_data=f"next_{end_index}")) - - if navigation_row: - markup.row(*navigation_row) - - # Кнопка отмены - markup.row(types.InlineKeyboardButton(text='Отмена', callback_data='cancel_active_triggers')) - - return markup - - -@bot.callback_query_handler( - func=lambda call: call.data.startswith("region_") or call.data.startswith("prev_") or call.data.startswith( - "next_")) -def handle_region_pagination(call): - chat_id = call.message.chat.id - message_id = call.message.message_id - data = call.data - - regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов - regions_per_page = 10 - - # Если был выбран регион, то убираем клавиатуру и продолжаем выполнение функции - if data.startswith("region_"): - region_id = data.split("_")[1] - telebot.logger.debug(region_id) - bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) - handle_region_selection(call, region_id) # Продолжаем выполнение функции после выбора региона - - # Если была нажата кнопка для переключения страниц - elif data.startswith("prev_") or data.startswith("next_"): - direction, index = data.split("_") - index = int(index) - - # Рассчитываем новый индекс страницы - start_index = max(0, index - regions_per_page) if direction == "prev" else min(len(regions) - regions_per_page, - index) - - # Обновляем клавиатуру для новой страницы - markup = create_region_keyboard(regions, start_index, regions_per_page) - bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=markup) - - bot.answer_callback_query(call.id) - - -# Фаза 2: Обработка выбора региона и предложить выбор группы -def handle_region_selection(call, region_id): - chat_id = call.message.chat.id - telebot.logger.debug(f"{type(region_id)}, {region_id}, {call.data}") - try: - # Получаем группы хостов для выбранного региона - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - - host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) - filtered_groups = [group for group in host_groups if - 'test' not in group['name'].lower() and f'_{region_id}' in group['name']] - - - # Если нет групп - if not filtered_groups: - backend_bot.bot.send_message(chat_id, "Нет групп хостов для этого региона.") - show_main_menu(chat_id) - return - - # Создаем клавиатуру с выбором группы или всех групп - markup = types.InlineKeyboardMarkup() - for group in filtered_groups: - markup.add(types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) - markup.add(types.InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", - callback_data=f"all_groups_{region_id}")) - - backend_bot.bot.send_message(chat_id, "Выберите группу хостов или получите события по всем группам региона:", - reply_markup=markup) - except Exception as e: - backend_bot.bot.send_message(chat_id, f"Ошибка при подключении к Zabbix API.\n{str(e)}") - show_main_menu(chat_id) - - -# Фаза 3: Обработка выбора группы/всех групп и запрос периода -@bot.callback_query_handler(func=lambda call: call.data.startswith("group_") or call.data.startswith("all_groups_")) -def handle_group_or_all_groups(call): - chat_id = call.message.chat.id - message_id = call.message.message_id - - # Убираем клавиатуру после выбора группы - bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) - - # Если выбрана конкретная группа - if call.data.startswith("group_"): - group_id = call.data.split("_")[1] - get_triggers_for_group(chat_id, group_id) # Сразу получаем события для группы - show_main_menu(chat_id) - - # Если выбраны все группы региона - elif call.data.startswith("all_groups_"): - region_id = call.data.split("_")[2] - get_triggers_for_all_groups(chat_id, region_id) # Сразу получаем события для всех групп региона - show_main_menu(chat_id) - - -# def run_polling(): -# bot.infinity_polling(timeout=10, long_polling_timeout=5) - - -# Запуск Flask-приложения -# def run_flask(): -# app.run(port=5000, host='0.0.0.0', debug=True, use_reloader=False) - - -# # Основная функция для запуска -# def main(): -# # Инициализация базы данных -# # bot_database.init_db() -# # Запуск Flask и бота в отдельных потоках -# -# Thread(target=run_flask, daemon=True).start() -# Thread(target=run_polling, daemon=True).start() -# # Запуск асинхронных задач -# -# asyncio.run(consume_from_queue()) - -def start_flask(): - app = create_app() - app.run(host="0.0.0.0", port=5000) - - -if __name__ == '__main__': - flask_process = Process(target=start_flask) - bot_process = Process(target=run_bot) - - flask_process.start() - bot_process.start() - - flask_process.join() - bot_process.join() diff --git a/utilities/log_manager.py b/utilities/log_manager.py deleted file mode 100644 index e68594f..0000000 --- a/utilities/log_manager.py +++ /dev/null @@ -1,133 +0,0 @@ -import logging -import sys -from logging.config import dictConfig - - -class UTF8StreamHandler(logging.StreamHandler): - def __init__(self, stream=None): - super().__init__(stream or sys.stdout) - self.setStream(stream or sys.stdout) - - def setStream(self, stream): - super().setStream(stream) - if hasattr(stream, 'reconfigure'): - stream.reconfigure(encoding='utf-8') - - -class FilterByMessage(logging.Filter): - def filter(self, record): - # Фильтруем сообщения, содержащие 'Received 1 new updates' - return 'Received ' not in record.getMessage() - - -class LogManager: - def __init__(self): - self.setup_logging() - - def setup_logging(self): - dictConfig({ - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'default': { - 'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s', - }, - 'debug': { - 'format': '[%(asctime)s] %(levelname)s %(module)s [%(funcName)s:%(lineno)d]: %(message)s' - } - }, - 'filters': { - 'filter_by_message': { - '()': FilterByMessage, - } - }, - 'handlers': { - 'console': { - '()': UTF8StreamHandler, - 'stream': 'ext://sys.stdout', - 'formatter': 'default', - 'filters': ['filter_by_message'], - }, - }, - 'root': { - 'level': 'WARNING', - 'handlers': ['console'] - }, - 'loggers': { - 'flask': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'telebot': { - 'level': 'INFO', - 'handlers': ['console'], - 'propagate': False, - }, - 'werkzeug': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'flask_ldap3_login': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'flask_login': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'pyzabbix': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'app': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'pika': { - 'level': 'INFO', - 'handlers': ['console'], - 'propagate': False, - }, - 'users_service': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'regions_service': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - 'debug': { - 'level': 'DEBUG', - 'handlers': ['console'], - 'propagate': False, - }, - } - }) - - def change_log_level(self, component, level): - """Changes the log level of a specified component.""" - if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: - return False, 'Invalid log level' - - log_level = getattr(logging, level, logging.DEBUG) - - if component in self.get_all_loggers(): - logger = logging.getLogger(component) - logger.setLevel(log_level) - for handler in logger.handlers: - handler.setLevel(log_level) - return True, f'Log level for {component} changed to {level}' - else: - return False, 'Invalid component' - - def get_all_loggers(self): - """Returns a list of all configured loggers.""" - return list(logging.Logger.manager.loggerDict.keys()) diff --git a/utilities/notification_manager.py b/utilities/notification_manager.py deleted file mode 100644 index 019be35..0000000 --- a/utilities/notification_manager.py +++ /dev/null @@ -1,42 +0,0 @@ -from utilities.rabbitmq import send_to_queue -from app.models import Regions, Subscriptions -from app.models import Users -from app.extensions.db import db - -class NotificationManager: - def __init__(self, logger): - self.logger = logger - - def get_subscribers(self, region_id, severity): - query = db.session.query(Users.chat_id, Users.telegram_id).join(Subscriptions).filter( - Subscriptions.region_id == region_id, - Subscriptions.active == True - ) - if severity != 'Disaster': - query = query.filter(Subscriptions.disaster_only == False) - - self.logger.debug(f"Выполнение запроса: {query} для региона {region_id}") - results = query.all() - self.logger.debug(f"Найдено подписчиков: {len(results)} для региона {region_id}") - return results - - def is_region_active(self, region_id): - region = Regions.query.get(region_id) - return region and region.active - - def send_notifications(self, subscribers, message): - undelivered = False - for chat_id, username in subscribers: - user = Users.query.get(chat_id) - if user and not user.is_blocked: - formatted_message = message.replace('\n', ' ').replace('\r', '') - self.logger.info(f"Формирование сообщения для пользователя {username} ({chat_id}) [{formatted_message}]") - try: - send_to_queue({'chat_id': chat_id, 'username': username, 'message': message}) - self.logger.debug(f"Сообщение поставлено в очередь для {username} ({chat_id})") - except Exception as e: - self.logger.error(f"Ошибка при отправке сообщения для {username} ({chat_id})): {e}") - undelivered = True - else: - self.logger.warning(f"Пользователь {username} ({chat_id}) заблокирован или не найден. Уведомление не отправлено.") - return undelivered \ No newline at end of file diff --git a/utilities/rabbitmq.py b/utilities/rabbitmq.py deleted file mode 100644 index 1150c71..0000000 --- a/utilities/rabbitmq.py +++ /dev/null @@ -1,122 +0,0 @@ -import asyncio -import json - -from flask import current_app -from app.models.users import Users -import aio_pika -import telebot -import pika - -import backend_bot - -from config import RABBITMQ_LOGIN, RABBITMQ_PASS, RABBITMQ_HOST, RABBITMQ_QUEUE, RABBITMQ_URL_FULL - -# Semaphore for rate limiting -rate_limit_semaphore = asyncio.Semaphore(25) - -def rabbitmq_connection(): - credentials = pika.PlainCredentials(RABBITMQ_LOGIN, RABBITMQ_PASS) - parameters = pika.ConnectionParameters( - host=RABBITMQ_HOST, - credentials=credentials, - heartbeat=600, - blocked_connection_timeout=300 - ) - connection = pika.BlockingConnection(parameters) - channel = connection.channel() - channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True) - return connection, channel - -def send_to_queue(message): - connection, channel = rabbitmq_connection() - channel.basic_publish( - exchange='', - routing_key=RABBITMQ_QUEUE, - body=json.dumps(message), - properties=pika.BasicProperties( - delivery_mode=2, - )) - connection.close() - -async def consume_from_queue(): - while True: - try: - connection = await aio_pika.connect_robust(RABBITMQ_URL_FULL) - async with connection: - channel = await connection.channel() - queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True) - - async for message in queue: - async with message.process(): - try: - data = json.loads(message.body.decode('utf-8')) - chat_id = data["chat_id"] - message_text = data["message"] - await send_notification_message(chat_id, message_text) - except (json.JSONDecodeError, KeyError) as e: - telebot.logger.error(f"Error processing message: {e}") - except Exception as e: - telebot.logger.error(f"Error sending message: {e}") - except aio_pika.exceptions.AMQPError as e: - telebot.logger.error(f"RabbitMQ error: {e}") - except Exception as e: - telebot.logger.error(f"Critical error: {e}") - finally: - await asyncio.sleep(5) - -# async def send_message(chat_id, message, is_notification=False): -# try: -# if is_notification: -# await rate_limit_semaphore.acquire() -# await asyncio.to_thread(backend_bot.bot.send_message, chat_id, message, parse_mode='HTML') -# formatted_message = message.replace('\n', ' ').replace('\r', '') # Добавляем форматирование сообщения -# telebot.logger.info(f'Send notification to {chat_id} from RabbitMQ [{formatted_message}]') # Добавляем логирование -# except telebot.apihelper.ApiTelegramException as e: -# if "429" in str(e): -# await asyncio.sleep(1) -# await send_message(chat_id, message, is_notification) -# else: -# telebot.logger.error(f"Failed to send message: {e}") -# except Exception as e: -# telebot.logger.error(f"Unexpected error: {e}") -# finally: -# if is_notification: -# rate_limit_semaphore.release() - -async def send_message(chat_id, message, is_notification=False): - telegram_id = "unknown" - try: - if is_notification: - await rate_limit_semaphore.acquire() - - # Получение telegram_id через app_context - def get_user(): - with current_app.app_context(): - user = Users.query.get(chat_id) - return user.telegram_id if user else "unknown" - - telegram_id = await asyncio.to_thread(get_user) - - # Отправка сообщения - await asyncio.to_thread(backend_bot.bot.send_message, chat_id, message, parse_mode='HTML') - - # Форматирование и лог - formatted_message = message.replace('\n', ' ').replace('\r', '') - telebot.logger.info(f'Send notification to {telegram_id} ({chat_id}) from RabbitMQ [{formatted_message}]') - - - - except telebot.apihelper.ApiTelegramException as e: - if "429" in str(e): - await asyncio.sleep(1) - await send_message(chat_id, message, is_notification) - else: - telebot.logger.error(f"Failed to send message to {telegram_id} ({chat_id}): {e}") - except Exception as e: - telebot.logger.error(f"Unexpected error sending message to {telegram_id} ({chat_id}): {e}") - finally: - if is_notification: - rate_limit_semaphore.release() - -async def send_notification_message(chat_id, message): - await send_message(chat_id, message, is_notification=True) \ No newline at end of file diff --git a/utilities/telegram_utilities.py b/utilities/telegram_utilities.py deleted file mode 100644 index fc1ce9f..0000000 --- a/utilities/telegram_utilities.py +++ /dev/null @@ -1,116 +0,0 @@ -import re -import time - -import telebot - -import backend_bot -import bot_database -import telezab - - -def validate_chat_id(chat_id): - """Validate that chat_id is composed only of digits.""" - return chat_id.isdigit() - - -def validate_telegram_id(telegram_id): - """Validate that telegram_id starts with '@'.""" - return telegram_id.startswith('@') - - -def validate_email(email): - """Validate that email domain is '@rtmis.ru'.""" - return re.match(r'^[\w.-]+@rtmis\.ru$', email) is not None - - -def format_message(data): - try: - priority_map = { - 'High': '⚠️', - 'Disaster': '⛔️' - } - priority = priority_map.get(data['severity']) - msg = escape_telegram_chars(data['msg']) - if data['status'].upper() == "PROBLEM": - message = ( - f"{priority} {data['host']} ({data['ip']})\n" - f"Описание: {msg}\n" - f"Критичность: {data['severity']}\n" - f"Время возникновения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" - ) - if 'link' in data: - message += f'URL: Ссылка на график' - return message - else: - message = ( - f"✅ {data['host']} ({data['ip']})\n" - f"Описание: {msg}\n" - f"Критичность: {data['severity']}\n" - f"Проблема устранена!\n" - f"Время устранения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" - ) - if 'link' in data: - message += f'URL: Ссылка на график' - return message - except KeyError as e: - raise ValueError(f"Missing key in data: {e}") - - -def extract_region_number(host): - # Используем регулярное выражение для извлечения цифр после первого символа и до первой буквы - match = re.match(r'^.\d+', host) - if match: - return match.group(0)[1:] # Возвращаем строку без первого символа - return None - - -def escape_telegram_chars(text): - """ - Экранирует запрещённые символы для Telegram API: - < -> < - > -> > - & -> & - Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием. - """ - replacements = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', # Для кавычек - } - - # Применяем замены - for char, replacement in replacements.items(): - text = text.replace(char, replacement) - - return text - - -def show_main_menu(chat_id): - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - if bot_database.is_whitelisted(chat_id): - telezab.state.set_state(chat_id, "MAIN_MENU") - markup.add('Настройки', 'Помощь', 'Активные события') - else: - telezab.state.set_state(chat_id, "REGISTRATION") - markup.add('Регистрация') - backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) - - -def create_settings_keyboard(): - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - markup.row('Подписаться', 'Отписаться') - markup.row('Мои подписки', 'Режим уведомлений') - markup.row('Назад') - return markup - - -def show_settings_menu(chat_id): - if not bot_database.is_whitelisted(chat_id): - telezab.state.set_state(chat_id, "REGISTRATION") - backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") - return - markup = create_settings_keyboard() - backend_bot.bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup) - - diff --git a/utilities/user_state_manager.py b/utilities/user_state_manager.py deleted file mode 100644 index 70ae951..0000000 --- a/utilities/user_state_manager.py +++ /dev/null @@ -1,16 +0,0 @@ -class UserStateManager: - def __init__(self): - # Словарь для хранения состояния каждого пользователя - self.user_states = {} - - def set_state(self, chat_id, state): - """Устанавливает состояние для пользователя.""" - self.user_states[chat_id] = state - - def get_state(self, chat_id): - """Получает текущее состояние пользователя.""" - return self.user_states.get(chat_id, "MAIN_MENU") - - def reset_state(self, chat_id): - """Сбрасывает состояние пользователя в главное меню.""" - self.user_states[chat_id] = "MAIN_MENU"