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
|
/db/telezab.db
|
||||||
/trash/
|
/trash/
|
||||||
/venv3.12.3/
|
/venv3.12.3/
|
||||||
|
.gitea/
|
||||||
|
.github/
|
||||||
|
.gitlab/
|
||||||
|
# Python bytecode
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Editor swap/temp files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.bak
|
||||||
|
*~
|
||||||
|
|
||||||
|
# SQLite journals (если используется)
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# Docker-related
|
||||||
|
Dockerfile.*
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Test/coverage artifacts
|
||||||
|
*.coverage
|
||||||
|
htmlcov/
|
||||||
|
.cache/
|
||||||
|
.coverage.*
|
||||||
|
.nox/
|
||||||
|
.tox/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|||||||
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"
|
LABEL authors="UdoChudo"
|
||||||
# Установим необходимые пакеты
|
|
||||||
|
# Установка системных зависимостей и очистка
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
|
||||||
libpq-dev \
|
|
||||||
gcc \
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
tzdata \
|
tzdata \
|
||||||
sqlite3 \
|
sqlite3 \
|
||||||
curl \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
telnet \
|
|
||||||
supervisor \
|
ENV TZ=Europe/Moscow
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Установим рабочую директорию
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Скопируем файлы проекта
|
COPY requirements.txt .
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
# Копируем конфигурацию supervisord
|
|
||||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
|
|
||||||
# Установим зависимости проекта
|
|
||||||
RUN mkdir -p /app/logs
|
|
||||||
RUN pip install --no-cache-dir -r 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
|
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
|
ENV APP_TYPE=telegram
|
||||||
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
|
||||||
|
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.routes import register_blueprints
|
||||||
from app.extensions.auth_ext import init_auth, login_manager
|
from app.extensions.auth_ext import init_auth, login_manager
|
||||||
|
|
||||||
import config
|
from config import Config
|
||||||
from app.routes.dashboard import dashboard_bp
|
from app.routes.dashboard import dashboard_bp
|
||||||
# from backend.api import bp_api
|
# from backend.api import bp_api
|
||||||
|
|
||||||
from config import TZ
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection SpellCheckingInspection
|
# noinspection SpellCheckingInspection
|
||||||
def create_app():
|
def create_app() -> Flask:
|
||||||
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
app.config.from_object(Config)
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
||||||
app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
# app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
# app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
# app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
# app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
# app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||||
app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
# app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||||
app.config['TIMEZONE'] = TZ
|
# app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||||
|
# app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||||
|
# app.config['TIMEZONE'] = TZ
|
||||||
|
|
||||||
# Инициализация расширений
|
# Инициализация расширений
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
init_auth(app)
|
init_auth(app)
|
||||||
|
|
||||||
# Инициализация AuditLogger с передачей db.session
|
# Инициализация AuditLogger с передачей db.session
|
||||||
app.audit_logger = AuditLogger(db.session)
|
app.audit_logger = AuditLogger(db.session)
|
||||||
|
|
||||||
@ -78,5 +79,3 @@ def create_app():
|
|||||||
|
|
||||||
return 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
|
# app/bot/handlers/help.py
|
||||||
|
from flask import Flask
|
||||||
from telebot.types import Message
|
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(commands=['help'])
|
||||||
@bot.message_handler(func=lambda msg: msg.text == "Помощь")
|
@bot.message_handler(func=lambda msg: msg.text == "Помощь")
|
||||||
def handle_help(message: Message):
|
def handle_help(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
|
||||||
help_text = (
|
help_text = (
|
||||||
'<b>/start</b> - Показать меню бота\n'
|
'ℹ️<b>/start</b> - Показать меню бота\n'
|
||||||
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
|
'ℹ️<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
|
||||||
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
|
'ℹ️<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
|
||||||
f'<b>Помощь</b> - <a href="{HELP_URL}">Описание всех возможностей бота</a>'
|
f'ℹ️<b>Помощь</b> - <a href="{HELP_URL}">Описание всех возможностей бота</a>')
|
||||||
)
|
|
||||||
bot.send_message(message.chat.id, help_text, parse_mode="HTML")
|
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 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 == "Регистрация")
|
@bot.message_handler(func=lambda msg: msg.text == "Регистрация")
|
||||||
def handle_registration(message: Message):
|
def handle_registration(message: Message):
|
||||||
chat_id = message.chat.id
|
chat_id = message.chat.id
|
||||||
username = message.from_user.username
|
username = f"{message.from_user.username}" if message.from_user.username else "N/A"
|
||||||
if username:
|
|
||||||
username = f"@{username}"
|
|
||||||
else:
|
|
||||||
username = "N/A"
|
|
||||||
text = (
|
text = (
|
||||||
f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n'
|
f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n'
|
||||||
f'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
|
f'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
|
||||||
f'В теле письма указать:\n'
|
f'В теле письма указать:\n'
|
||||||
f'1. <b>ФИО</b>\n'
|
f'1. <b>ФИО</b>\n'
|
||||||
f'2. <b>Ваш Chat ID</b>: {chat_id}\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")
|
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.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.main_menu import get_main_menu
|
||||||
from app.bot.keyboards.settings_menu import get_settings_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 register_handlers(bot,app, state_manager: UserStateManager):
|
||||||
def handle_unsubscribe(message: Message):
|
@bot.message_handler(commands=['settings'])
|
||||||
bot.send_message(message.chat.id, "🔕 Функция отписки ещё не реализована.")
|
@bot.message_handler(func=lambda msg: msg.text == "Настройки")
|
||||||
|
def handle_settings_menu(message: Message):
|
||||||
@bot.message_handler(func=lambda msg: msg.text == "Мои подписки")
|
chat_id = message.chat.id
|
||||||
def handle_my_subscriptions(message: Message):
|
username = f"{message.from_user.username}" if message.from_user.username else "N/A"
|
||||||
bot.send_message(message.chat.id, "📄 Отображение подписок пока не реализовано.")
|
with app.app_context():
|
||||||
|
if not auth(chat_id, app):
|
||||||
@bot.message_handler(func=lambda msg: msg.text == "Режим уведомлений")
|
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
|
||||||
def handle_notify_mode(message: Message):
|
logger.warning(f"Неавторизованный пользователь {chat_id} @{username}")
|
||||||
bot.send_message(message.chat.id, "⚙️ Настройка режима уведомлений пока не реализована.")
|
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 == "Назад")
|
@bot.message_handler(func=lambda msg: msg.text == "Назад")
|
||||||
def handle_back(message: Message):
|
def handle_back_button(message: Message):
|
||||||
bot.send_message(message.chat.id, "Возврат в главное меню", reply_markup=get_main_menu())
|
bot.send_message(message.chat.id,"Главное меню", reply_markup=get_main_menu())
|
||||||
@ -1,41 +1,27 @@
|
|||||||
# app/bot/handlers/start.py
|
# 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.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'])
|
@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
|
chat_id = message.chat.id
|
||||||
|
username = f"{message.from_user.username}" if message.from_user.username else "N/A"
|
||||||
if data:
|
user = check_registration(bot, message,app)
|
||||||
if data.get('user_verified'):
|
if not user:
|
||||||
user = data['user']
|
state_manager.set_state(chat_id, UserStates.REGISTRATION)
|
||||||
bot.send_message(
|
logger.warning(f"Неавторизованный пользователь {chat_id} @{username}")
|
||||||
chat_id,
|
|
||||||
f"👋 Привет, {user.user_email}!\nВыберите действие из меню:",
|
|
||||||
reply_markup=get_main_menu()
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
state_manager.set_state(chat_id, UserStates.MAIN_MENU)
|
||||||
|
bot.send_message(chat_id, f"👋 Привет, {username}!", reply_markup=get_main_menu())
|
||||||
|
|
||||||
|
|
||||||
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
|
from telebot.types import ReplyKeyboardMarkup, KeyboardButton
|
||||||
|
|
||||||
def get_settings_menu():
|
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("Мои подписки"),KeyboardButton("Режим уведомлений"))
|
markup.add(KeyboardButton("Мои подписки"),KeyboardButton("Режим уведомлений"))
|
||||||
markup.add(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
|
import telebot
|
||||||
from app.bot.config import BOT_TOKEN
|
import logging
|
||||||
from app.bot.handlers import start, main_menu, settings, help, registration
|
|
||||||
from app.bot.middlewares.user_access import UserVerificationMiddleware
|
|
||||||
from app import create_app
|
|
||||||
|
|
||||||
|
logger = telebot.logger # Используем логгер telebot
|
||||||
|
logger.setLevel(logging.INFO) # Уровень логов
|
||||||
|
|
||||||
bot = telebot.TeleBot(BOT_TOKEN, use_class_middlewares=True, parse_mode='HTML')
|
def run_bot(app, bot):
|
||||||
flask_app = create_app()
|
# Перед запуском polling нужно push app_context, чтобы работал Flask
|
||||||
|
app.app_context().push()
|
||||||
# Регистрируем обработчики
|
|
||||||
start.register_handlers(bot)
|
|
||||||
main_menu.register_handlers(bot)
|
|
||||||
settings.register_handlers(bot)
|
|
||||||
help.register_handlers(bot)
|
|
||||||
registration.register_handlers(bot)
|
|
||||||
|
|
||||||
# Потом подключаем middleware
|
|
||||||
user_verification_middleware = UserVerificationMiddleware(bot, flask_app)
|
|
||||||
bot.setup_middleware(user_verification_middleware)
|
|
||||||
|
|
||||||
def run_bot():
|
|
||||||
bot.infinity_polling()
|
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
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
db: SQLAlchemy = 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):
|
class UserEvents(db.Model):
|
||||||
|
__tablename__ = "user_events"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
chat_id = db.Column(db.Integer, nullable=False)
|
chat_id = db.Column(db.Integer, nullable=False)
|
||||||
telegram_id = db.Column(db.String(80), nullable=False)
|
telegram_id = db.Column(db.String(80), nullable=False)
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.notifications_service import NotificationService
|
import app.extensions.rabbitmq as rabbitmq_mod
|
||||||
|
import json
|
||||||
notification_bp = Blueprint('notification', __name__,url_prefix='/notifications')
|
|
||||||
|
|
||||||
|
notification_bp = Blueprint('notification', __name__, url_prefix='/notification')
|
||||||
|
|
||||||
@notification_bp.route('/', methods=['POST'], strict_slashes=False)
|
@notification_bp.route('/', methods=['POST'], strict_slashes=False)
|
||||||
def notification():
|
def send_notification():
|
||||||
service = NotificationService()
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
result, status = service.process_notification(data)
|
if not data:
|
||||||
return jsonify(result), status
|
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.notification_manager import NotificationManager
|
||||||
from utilities.telegram_utilities import extract_region_number, format_message
|
# from utilities.telegram_utilities import extract_region_number, format_message
|
||||||
from flask import current_app
|
# from flask import current_app
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
class NotificationService:
|
# class NotificationService:
|
||||||
def __init__(self):
|
# def __init__(self):
|
||||||
self.logger = current_app.logger
|
# self.logger = current_app.logger
|
||||||
self.manager = NotificationManager(self.logger)
|
# self.manager = NotificationManager(self.logger)
|
||||||
|
#
|
||||||
def process_notification(self, data):
|
# def process_notification(self, data):
|
||||||
self.logger.info(f"Получены данные уведомления: {data}")
|
# self.logger.info(f"Получены данные уведомления: {data}")
|
||||||
|
#
|
||||||
region_id = extract_region_number(data.get("host"))
|
# region_id = extract_region_number(data.get("host"))
|
||||||
if region_id is None:
|
# if region_id is None:
|
||||||
self.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
# self.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
||||||
return {"status": "error", "message": "Invalid host format"}, 400
|
# return {"status": "error", "message": "Invalid host format"}, 400
|
||||||
|
#
|
||||||
self.logger.debug(f"Извлечён номер региона: {region_id}")
|
# self.logger.debug(f"Извлечён номер региона: {region_id}")
|
||||||
|
#
|
||||||
subscribers = self.manager.get_subscribers(region_id, data['severity'])
|
# subscribers = self.manager.get_subscribers(region_id, data['severity'])
|
||||||
|
#
|
||||||
if self.manager.is_region_active(region_id):
|
# if self.manager.is_region_active(region_id):
|
||||||
message = format_message(data)
|
# message = format_message(data)
|
||||||
self.manager.send_notifications(subscribers, message)
|
# self.manager.send_notifications(subscribers, message)
|
||||||
|
#
|
||||||
return {"status": "success"}, 200
|
# return {"status": "success"}, 200
|
||||||
|
|||||||
@ -6,9 +6,9 @@ from typing import Dict, List, Optional, Tuple, Any
|
|||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy import or_
|
|
||||||
|
|
||||||
from app import db
|
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.models import Users # Предполагаем, что app.models/__init__.py экспортирует Users
|
||||||
from app.extensions.audit_logger import AuditLogger
|
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,
|
auditlog.users(action_type="add", actor_display_name=actor_user.display_name,
|
||||||
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
ldap_user_id=actor_user.id, affected_chat_id=chat_id,
|
||||||
telegram_id=telegram_id, email=user_email)
|
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
|
return {'message': 'Пользователь добавлен успешно'}, 201
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
db.session.rollback()
|
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}")
|
|
||||||
65
config.py
65
config.py
@ -1,22 +1,41 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
DEV = os.getenv('DEV')
|
#Настройки телеграм
|
||||||
TOKEN = os.getenv('TELEGRAM_TOKEN')
|
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_API_TOKEN = os.getenv('ZABBIX_API_TOKEN')
|
||||||
ZABBIX_URL = os.getenv('ZABBIX_URL')
|
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__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
DB_ABS_PATH = os.path.join(basedir, 'db/telezab.db')
|
DB_ABS_PATH = os.path.join(basedir, 'db/telezab.db')
|
||||||
SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
|
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}'
|
||||||
BASE_URL = '/telezab'
|
SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL", "shiftsupport-rtmis@rtmis.ru")
|
||||||
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
|
HELP_URL = os.getenv("HELP_URL", "https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183")
|
||||||
RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN')
|
#Настройки RabbitMQ
|
||||||
RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
|
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost')
|
||||||
RABBITMQ_QUEUE = 'telegram_notifications'
|
RABBITMQ_PORT = int(os.environ.get("RABBITMQ_PORT", "5672"))
|
||||||
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/"
|
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_HOST = os.getenv('LDAP_HOST', 'localhost')
|
||||||
LDAP_PORT = int(os.getenv('LDAP_PORT', 389))
|
LDAP_PORT = int(os.getenv('LDAP_PORT', 389))
|
||||||
LDAP_USE_SSL = os.getenv('LDAP_USE_SSL', 'False').lower() == 'true'
|
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_LOGIN_ATTR = os.getenv('LDAP_USER_LOGIN_ATTR', 'sAMAccountName')
|
||||||
LDAP_USER_SEARCH_SCOPE = os.getenv('LDAP_USER_SEARCH_SCOPE', 'SUBTREE')
|
LDAP_USER_SEARCH_SCOPE = os.getenv('LDAP_USER_SEARCH_SCOPE', 'SUBTREE')
|
||||||
LDAP_SCHEMA = os.getenv('LDAP_SCHEMA', 'active_directory')
|
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)
|
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')
|
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
|
||||||
PERMANENT_SESSION_LIFETIME = timedelta(seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600)))
|
PERMANENT_SESSION_LIFETIME = timedelta(
|
||||||
SESSION_REFRESH_EACH_REQUEST = os.getenv('SESSION_REFRESH_EACH_REQUEST',False)
|
seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600))
|
||||||
SESSION_COOKIE_MAX_AGE = os.getenv('SESSION_COOKIE_MAX_AGE',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
|
SQLAlchemy~=2.0.40
|
||||||
Flask~=3.1.0
|
Flask~=3.1.0
|
||||||
Flask-Login~=0.6.3
|
Flask-Login~=0.6.3
|
||||||
|
Flask-SQLAlchemy~=3.1.1
|
||||||
|
Flask-ldap3-login~=1.0.2
|
||||||
Werkzeug~=3.1.3
|
Werkzeug~=3.1.3
|
||||||
aio-pika~=9.5.5
|
aio-pika~=9.5.5
|
||||||
pika~=1.3.2
|
pika~=1.3.2
|
||||||
pytz~=2025.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