refactor: modularize Telegram bot and add RabbitMQ client foundation
All checks were successful
Build and Push Docker Images / build (push) Successful in 1m28s
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:
parent
52e31864b3
commit
ccb47d527f
@ -10,3 +10,37 @@
|
||||
/db/telezab.db
|
||||
/trash/
|
||||
/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/
|
||||
|
||||
32
.gitea/workflows/gitea-ci.yml
Normal file
32
.gitea/workflows/gitea-ci.yml
Normal 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}
|
||||
55
Dockerfile
55
Dockerfile
@ -1,37 +1,44 @@
|
||||
FROM python:3.13.1-slim
|
||||
# syntax=docker/dockerfile:1.4
|
||||
FROM python:3.13.1-slim AS base
|
||||
LABEL authors="UdoChudo"
|
||||
# Установим необходимые пакеты
|
||||
|
||||
# Установка системных зависимостей и очистка
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
tzdata \
|
||||
sqlite3 \
|
||||
curl \
|
||||
telnet \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV TZ=Europe/Moscow
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Установим рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Скопируем файлы проекта
|
||||
COPY . /app
|
||||
|
||||
# Копируем конфигурацию supervisord
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Установим зависимости проекта
|
||||
RUN mkdir -p /app/logs
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir gunicorn==23.0.0
|
||||
# Откроем порт для нашего приложения
|
||||
|
||||
COPY . .
|
||||
|
||||
# ====================
|
||||
# Образ для Flask
|
||||
# ====================
|
||||
FROM base AS flask
|
||||
|
||||
ENV APP_TYPE=flask
|
||||
ENV FLASK_APP=app
|
||||
|
||||
EXPOSE 5000
|
||||
ENV TZ=Europe/Moscow
|
||||
ENV FLASK_APP telezab.py
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["gunicorn --access-logfile - --error-logfile - -b 0.0.0.0:5000 'app:create_app()'"]
|
||||
|
||||
# ====================
|
||||
# Образ для Telegram бота
|
||||
# ====================
|
||||
FROM base AS telegram
|
||||
|
||||
# Указываем команду для запуска supervisord
|
||||
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
ENV APP_TYPE=telegram
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
CMD ["python run_telegram.py"]
|
||||
|
||||
@ -8,32 +8,33 @@ from app.models.user import User
|
||||
from app.routes import register_blueprints
|
||||
from app.extensions.auth_ext import init_auth, login_manager
|
||||
|
||||
import config
|
||||
from config import Config
|
||||
from app.routes.dashboard import dashboard_bp
|
||||
# from backend.api import bp_api
|
||||
|
||||
from config import TZ
|
||||
|
||||
|
||||
|
||||
# noinspection SpellCheckingInspection
|
||||
def create_app():
|
||||
def create_app() -> Flask:
|
||||
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||
app.config['TIMEZONE'] = TZ
|
||||
app.config.from_object(Config)
|
||||
|
||||
# app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
# app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||
# app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||
# app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||
# app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
# app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
# app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
# app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||
# app.config['TIMEZONE'] = TZ
|
||||
|
||||
# Инициализация расширений
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
init_auth(app)
|
||||
|
||||
# Инициализация AuditLogger с передачей db.session
|
||||
app.audit_logger = AuditLogger(db.session)
|
||||
|
||||
@ -78,5 +79,3 @@ def create_app():
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
@ -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
12
app/bot/constants.py
Normal 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()
|
||||
@ -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)
|
||||
49
app/bot/handlers/active_triggers.py
Normal file
49
app/bot/handlers/active_triggers.py
Normal 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) # обязательно, чтобы убрать "часики"
|
||||
|
||||
22
app/bot/handlers/cancel_input.py
Normal file
22
app/bot/handlers/cancel_input.py
Normal 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
39
app/bot/handlers/debug.py
Normal 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, "❌ Неизвестный уровень логирования")
|
||||
@ -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())
|
||||
|
||||
@ -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()
|
||||
)
|
||||
24
app/bot/handlers/my_subscriptions.py
Normal file
24
app/bot/handlers/my_subscriptions.py
Normal 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)
|
||||
72
app/bot/handlers/notification_switch_mode.py
Normal file
72
app/bot/handlers/notification_switch_mode.py
Normal 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())
|
||||
|
||||
|
||||
|
||||
@ -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")
|
||||
@ -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())
|
||||
@ -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, "Произошла ошибка. Попробуйте позже.")
|
||||
|
||||
46
app/bot/handlers/subscribe.py
Normal file
46
app/bot/handlers/subscribe.py
Normal 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)
|
||||
|
||||
15
app/bot/handlers/template_settings.py
Normal file
15
app/bot/handlers/template_settings.py
Normal 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())
|
||||
35
app/bot/handlers/unsubscribe.py
Normal file
35
app/bot/handlers/unsubscribe.py
Normal 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
12
app/bot/i18n/messages.py
Normal 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",
|
||||
}
|
||||
}
|
||||
39
app/bot/keyboards/active_triggers.py
Normal file
39
app/bot/keyboards/active_triggers.py
Normal 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
|
||||
@ -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("Назад"))
|
||||
|
||||
@ -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} прошёл проверку")
|
||||
56
app/bot/processors/active_triggers_processor.py
Normal file
56
app/bot/processors/active_triggers_processor.py
Normal 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")
|
||||
39
app/bot/processors/my_subscriptions_processor.py
Normal file
39
app/bot/processors/my_subscriptions_processor.py
Normal 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())
|
||||
|
||||
75
app/bot/processors/subscribe_processor.py
Normal file
75
app/bot/processors/subscribe_processor.py
Normal 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())
|
||||
|
||||
65
app/bot/processors/unsubscribe_processor.py
Normal file
65
app/bot/processors/unsubscribe_processor.py
Normal 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())
|
||||
22
app/bot/services/mailing_service/__init__.py
Normal file
22
app/bot/services/mailing_service/__init__.py
Normal 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()
|
||||
51
app/bot/services/mailing_service/composer.py
Normal file
51
app/bot/services/mailing_service/composer.py
Normal 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}")
|
||||
38
app/bot/services/mailing_service/db_utils.py
Normal file
38
app/bot/services/mailing_service/db_utils.py
Normal 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]
|
||||
151
app/bot/services/mailing_service/mailing_consumer.py
Normal file
151
app/bot/services/mailing_service/mailing_consumer.py
Normal 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()
|
||||
19
app/bot/services/mailing_service/parser.py
Normal file
19
app/bot/services/mailing_service/parser.py
Normal 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
|
||||
8
app/bot/services/mailing_service/recepient_resolver.py
Normal file
8
app/bot/services/mailing_service/recepient_resolver.py
Normal 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)
|
||||
@ -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")
|
||||
@ -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():
|
||||
def run_bot(app, bot):
|
||||
# Перед запуском polling нужно push app_context, чтобы работал Flask
|
||||
app.app_context().push()
|
||||
bot.infinity_polling()
|
||||
0
app/bot/utils/__init__.py
Normal file
0
app/bot/utils/__init__.py
Normal file
32
app/bot/utils/auth.py
Normal file
32
app/bot/utils/auth.py
Normal 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
0
app/bot/utils/cancel.py
Normal file
55
app/bot/utils/helpers.py
Normal file
55
app/bot/utils/helpers.py
Normal 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
31
app/bot/utils/regions.py
Normal 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
30
app/bot/utils/tg_audit.py
Normal 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}")
|
||||
20
app/bot/utils/tg_escape_chars.py
Normal file
20
app/bot/utils/tg_escape_chars.py
Normal file
@ -0,0 +1,20 @@
|
||||
def escape_telegram_chars(text):
|
||||
"""
|
||||
Экранирует запрещённые символы для Telegram API:
|
||||
< -> <
|
||||
> -> >
|
||||
& -> &
|
||||
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
|
||||
"""
|
||||
replacements = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"', # Для кавычек
|
||||
}
|
||||
|
||||
# Применяем замены
|
||||
for char, replacement in replacements.items():
|
||||
text = text.replace(char, replacement)
|
||||
|
||||
return text
|
||||
35
app/bot/utils/tg_formatter.py
Normal file
35
app/bot/utils/tg_formatter.py
Normal 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
119
app/bot/utils/zabbix.py
Normal 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 []
|
||||
25
app/extensions/bot_send.py
Normal file
25
app/extensions/bot_send.py
Normal 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
|
||||
@ -1,3 +1,4 @@
|
||||
#app/extensions/db.py
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db: SQLAlchemy = SQLAlchemy()
|
||||
63
app/extensions/rabbitmq.py
Normal file
63
app/extensions/rabbitmq.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}")
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
198
backend_bot.py
198
backend_bot.py
@ -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)
|
||||
@ -1,4 +0,0 @@
|
||||
import telebot
|
||||
from config import TOKEN
|
||||
|
||||
bot = telebot.TeleBot(TOKEN)
|
||||
@ -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
|
||||
@ -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}")
|
||||
67
config.py
67
config.py
@ -1,22 +1,41 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
DEV = os.getenv('DEV')
|
||||
#Настройки телеграм
|
||||
TOKEN = os.getenv('TELEGRAM_TOKEN')
|
||||
ADMINS = os.getenv('TELEGRAM_ADMINS', '')
|
||||
ADMINS_LIST = [int(admin_id.strip()) for admin_id in ADMINS.split(',') if admin_id.strip().isdigit()]
|
||||
REGIONS_PER_PAGE = os.getenv('REGIONS_PER_PAGE', 10)
|
||||
#Настройки Zabbix
|
||||
ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN')
|
||||
ZABBIX_URL = os.getenv('ZABBIX_URL')
|
||||
DB_PATH = 'db/telezab.db'
|
||||
ZABBIX_VERIFY_SSL = os.getenv('ZABBIX_VERIFY_SSL', True)
|
||||
ZABBIX_TZ = os.getenv('ZABBIX_TZ', 'Europe/Moscow')
|
||||
#Настройки Flask и Telegram bot
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
DB_ABS_PATH = os.path.join(basedir, 'db/telezab.db')
|
||||
SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
|
||||
BASE_URL = '/telezab'
|
||||
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
|
||||
RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN')
|
||||
RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
|
||||
RABBITMQ_QUEUE = 'telegram_notifications'
|
||||
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/"
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}'
|
||||
SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL", "shiftsupport-rtmis@rtmis.ru")
|
||||
HELP_URL = os.getenv("HELP_URL", "https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183")
|
||||
#Настройки RabbitMQ
|
||||
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost')
|
||||
RABBITMQ_PORT = int(os.environ.get("RABBITMQ_PORT", "5672"))
|
||||
RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN', 'admin')
|
||||
RABBITMQ_PASS = os.getenv('RABBITMQ_PASS', 'admin')
|
||||
RABBITMQ_QUEUE = os.environ.get("RABBITMQ_QUEUE", "telegram_notifications")
|
||||
RABBITMQ_NOTIFICATIONS_QUEUE = os.environ.get("RABBITMQ_NOTIFICATIONS_QUEUE", "notifications_queue")
|
||||
RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST", "/")
|
||||
RABBITMQ_VHOST_ENCODED = quote_plus(RABBITMQ_VHOST)
|
||||
|
||||
# Настройки LDAP
|
||||
RABBITMQ_URL_FULL = (
|
||||
f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/{RABBITMQ_VHOST_ENCODED}"
|
||||
)
|
||||
# Mailing settings
|
||||
MAILING_MAX_WORKERS = int(os.environ.get("MAILING_MAX_WORKERS", "16"))
|
||||
MAILING_RATE_LIMIT = int(os.environ.get("MAILING_RATE_LIMIT", "25"))
|
||||
|
||||
# Настройки Flask-LDAP3-login
|
||||
LDAP_HOST = os.getenv('LDAP_HOST', 'localhost')
|
||||
LDAP_PORT = int(os.getenv('LDAP_PORT', 389))
|
||||
LDAP_USE_SSL = os.getenv('LDAP_USE_SSL', 'False').lower() == 'true'
|
||||
@ -29,15 +48,25 @@ LDAP_USER_RDN_ATTR = os.getenv('LDAP_USER_RDN_ATTR', 'sAMAccountName')
|
||||
LDAP_USER_LOGIN_ATTR = os.getenv('LDAP_USER_LOGIN_ATTR', 'sAMAccountName')
|
||||
LDAP_USER_SEARCH_SCOPE = os.getenv('LDAP_USER_SEARCH_SCOPE', 'SUBTREE')
|
||||
LDAP_SCHEMA = os.getenv('LDAP_SCHEMA', 'active_directory')
|
||||
TZ = os.getenv('TZ', 'Europe/Moscow')
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}'
|
||||
|
||||
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', True)
|
||||
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY',True)
|
||||
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE','Lax')
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600)))
|
||||
SESSION_REFRESH_EACH_REQUEST = os.getenv('SESSION_REFRESH_EACH_REQUEST',False)
|
||||
SESSION_COOKIE_MAX_AGE = os.getenv('SESSION_COOKIE_MAX_AGE',3600)
|
||||
|
||||
class Config:
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# SQLAlchemy
|
||||
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(basedir, 'db/telezab.db')}"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
# Flask session
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key')
|
||||
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', 'True').lower() == 'true'
|
||||
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY', 'True').lower() == 'true'
|
||||
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(
|
||||
seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600))
|
||||
)
|
||||
SESSION_COOKIE_MAX_AGE = int(os.getenv('SESSION_COOKIE_MAX_AGE', 3600))
|
||||
|
||||
# Дополнительное (если используется)
|
||||
TIMEZONE = os.getenv('TZ', 'Europe/Moscow')
|
||||
|
||||
71
handlers.py
71
handlers.py
@ -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}).")
|
||||
@ -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
7
run_flask.py
Normal 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
19
run_telegram.py
Normal 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)
|
||||
@ -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
|
||||
310
telezab.py
310
telezab.py
@ -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()
|
||||
@ -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())
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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:
|
||||
< -> <
|
||||
> -> >
|
||||
& -> &
|
||||
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
|
||||
"""
|
||||
replacements = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"', # Для кавычек
|
||||
}
|
||||
|
||||
# Применяем замены
|
||||
for char, replacement in replacements.items():
|
||||
text = text.replace(char, replacement)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def show_main_menu(chat_id):
|
||||
markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
|
||||
if bot_database.is_whitelisted(chat_id):
|
||||
telezab.state.set_state(chat_id, "MAIN_MENU")
|
||||
markup.add('Настройки', 'Помощь', 'Активные события')
|
||||
else:
|
||||
telezab.state.set_state(chat_id, "REGISTRATION")
|
||||
markup.add('Регистрация')
|
||||
backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup)
|
||||
|
||||
|
||||
def create_settings_keyboard():
|
||||
markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
|
||||
markup.row('Подписаться', 'Отписаться')
|
||||
markup.row('Мои подписки', 'Режим уведомлений')
|
||||
markup.row('Назад')
|
||||
return markup
|
||||
|
||||
|
||||
def show_settings_menu(chat_id):
|
||||
if not bot_database.is_whitelisted(chat_id):
|
||||
telezab.state.set_state(chat_id, "REGISTRATION")
|
||||
backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота")
|
||||
return
|
||||
markup = create_settings_keyboard()
|
||||
backend_bot.bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup)
|
||||
|
||||
|
||||
@ -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"
|
||||
Loading…
x
Reference in New Issue
Block a user