refactor: modularize Telegram bot and add RabbitMQ client foundation
All checks were successful
Build and Push Docker Images / build (push) Successful in 1m28s

- Рефакторинг Telegram бота на модульную структуру для удобства поддержки и расширения
- Создан общий RabbitMQ клиент для Flask и Telegram компонентов
- Подготовлена базовая архитектура для будущего масштабирования и новых функций

Signed-off-by: UdoChudo <stream@udochudo.ru>
This commit is contained in:
Udo Chudo 2025-06-16 09:08:46 +05:00
parent 52e31864b3
commit ccb47d527f
72 changed files with 1677 additions and 1675 deletions

View File

@ -9,4 +9,38 @@
/db/
/db/telezab.db
/trash/
/venv3.12.3/
/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/

View File

@ -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}

View File

@ -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"]
ENV APP_TYPE=telegram
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["python run_telegram.py"]

View File

@ -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()

View File

@ -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}/"

12
app/bot/constants.py Normal file
View File

@ -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()

View File

@ -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)

View File

@ -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) # обязательно, чтобы убрать "часики"

View File

@ -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())

39
app/bot/handlers/debug.py Normal file
View File

@ -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, "❌ Неизвестный уровень логирования")

View File

@ -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 = (
'<b>/start</b> - Показать меню бота\n'
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
f'<b>Помощь</b> - <a href="{HELP_URL}">Описание всех возможностей бота</a>'
)
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 = (
'<b>/start</b> - Показать меню бота\n'
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
f'<b>Помощь</b> - <a href="{HELP_URL}">Описание всех возможностей бота</a>')
bot.send_message(message.chat.id, help_text, parse_mode="HTML", reply_markup=get_main_menu())

View File

@ -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()
)

View File

@ -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)

View File

@ -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.⛔️<b>Критические события</b> (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\n'
'В Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n'
'2.⚠️<b>Все события (По умолчанию)</b> - критические события, а также события 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())

View File

@ -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'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
f'В теле письма указать:\n'
f'1. <b>ФИО</b>\n'
f'2. <b>Ваш Chat ID</b>: {chat_id}\n'
f'3. <b>Ваше имя пользователя</b>: {username}')
f'3. <b>Ваше имя пользователя</b>: @{username}')
bot.send_message(chat_id, text, parse_mode="HTML")

View File

@ -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())

View File

@ -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, "Произошла ошибка. Попробуйте позже.")

View File

@ -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)

View File

@ -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())

View File

@ -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)

12
app/bot/i18n/messages.py Normal file
View File

@ -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",
}
}

View File

@ -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

View File

@ -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("Назад"))

View File

@ -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} прошёл проверку")

View File

@ -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")

View File

@ -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())

View File

@ -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())

View File

@ -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())

View File

@ -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()

View File

@ -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"<b>Описание</b>: {msg}\n"
f"<b>Критичность</b>: {severity}\n"
f"<b>Время возникновения</b>: {time_str} Мск\n"
)
else:
message = (
f"{host} ({ip})\n"
f"<b>Описание</b>: {msg}\n"
f"<b>Критичность</b>: {severity}\n"
f"<b>Проблема устранена!</b>\n"
f"<b>Время устранения</b>: {time_str} Мск\n"
)
link = data.get("link")
return message, link
except KeyError as e:
raise ValueError(f"Missing key in data: {e}")

View File

@ -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]

View File

@ -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()

View File

@ -0,0 +1,19 @@
# parser.py
import re
def parse_region_id(host: str) -> int | None:
"""
Извлекает region_id из строки host.
Формат: p<region><...>, например 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

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

32
app/bot/utils/auth.py Normal file
View File

@ -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'В теме письма указать "<b>Подтверждение регистрации в боте TeleZab</b>".\n'
f'В теле письма указать:\n'
f'1. <b>ФИО</b>\n'
f'2. <b>Ваш Chat ID</b>: {chat_id}\n'
f'3. <b>Ваше имя пользователя</b>: @{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

0
app/bot/utils/cancel.py Normal file
View File

55
app/bot/utils/helpers.py Normal file
View File

@ -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)

31
app/bot/utils/regions.py Normal file
View File

@ -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)

30
app/bot/utils/tg_audit.py Normal file
View File

@ -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}")

View File

@ -0,0 +1,20 @@
def escape_telegram_chars(text):
"""
Экранирует запрещённые символы для Telegram API:
< -> &lt;
> -> &gt;
& -> &amp;
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
"""
replacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;', # Для кавычек
}
# Применяем замены
for char, replacement in replacements.items():
text = text.replace(char, replacement)
return text

View File

@ -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"<b>Host</b>: {host}\n"
f"<b>Описание</b>: {description}\n"
f"<b>Критичность</b>: {priority}\n"
f"<b>Время создания</b>: {event_time_formatted}\n"
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>'
)

119
app/bot/utils/zabbix.py Normal file
View File

@ -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"<b>Host</b>: {host}\n"
f"<b>Описание</b>: {description}\n"
f"<b>Критичность</b>: {priority}\n"
f"<b>Время создания</b>: {event_time_formatted}\n"
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>')
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 []

View File

@ -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

View File

@ -1,3 +1,4 @@
#app/extensions/db.py
from flask_sqlalchemy import SQLAlchemy
db: SQLAlchemy = SQLAlchemy()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -1,4 +0,0 @@
import telebot
from config import TOKEN
bot = telebot.TeleBot(TOKEN)

View File

@ -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"<b>Host</b>: {host}\n"
f"<b>Описание</b>: {description}\n"
f"<b>Критичность</b>: {priority}\n"
f"<b>Время создания</b>: {event_time_formatted}\n"
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>')
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

View File

@ -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}")

View File

@ -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')

View File

@ -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. <b>Критические события</b> (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n'
'2. <b>Все события (По умолчанию)</b> - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.',
reply_markup=markup, parse_mode="HTML")
telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).")

View File

@ -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
requests~=2.32.3
gunicorn~=23.0.0

7
run_flask.py Normal file
View File

@ -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)

19
run_telegram.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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 = (
'<b>/start</b> - Показать меню бота\n'
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
'<b>Помощь</b> - <a href="https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183">Описание всех возможностей бота</a>'
)
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'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
f'В теле письма указать:\n'
f'1. <b>ФИО</b>\n'
f'2. <b>Ваш Chat ID</b>: {chat_id}\n'
f'3. <b>Ваше имя пользователя</b>: {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()

View File

@ -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())

View File

@ -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

View File

@ -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)

View File

@ -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"<b>Описание</b>: {msg}\n"
f"<b>Критичность</b>: {data['severity']}\n"
f"<b>Время возникновения</b>: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n"
)
if 'link' in data:
message += f'<b>URL</b>: <a href="{data['link']}">Ссылка на график</a>'
return message
else:
message = (
f"{data['host']} ({data['ip']})\n"
f"<b>Описание</b>: {msg}\n"
f"<b>Критичность</b>: {data['severity']}\n"
f"<b>Проблема устранена!</b>\n"
f"<b>Время устранения</b>: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n"
)
if 'link' in data:
message += f'<b>URL</b>: <a href="{data['link']}">Ссылка на график</a>'
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:
< -> &lt;
> -> &gt;
& -> &amp;
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
"""
replacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;', # Для кавычек
}
# Применяем замены
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)

View File

@ -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"