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"