From 8a8cf8af309831ea3370114b92610778305d8ce8 Mon Sep 17 00:00:00 2001 From: UdoChudo Date: Sun, 23 Feb 2025 14:27:48 +0500 Subject: [PATCH] Refactoring and cleanup codebase (starting...) --- backend_bot.py | 177 +++++ backend_flask.py | 348 +++++++++ backend_locks.py | 9 + backend_zabbix.py | 128 ++++ bot_database.py | 238 +++++++ config.py | 15 + log_manager.py | 11 +- rabbitmq.py | 155 ++++ rabbitmq_worker.py | 90 --- requirements.txt | Bin 972 -> 567 bytes telezab.py | 1614 +++--------------------------------------- templates/index.html | 10 + utils.py | 117 +++ webui/__init__.py | 19 + webui/index.py | 13 + zabbix_manager.py | 116 --- 16 files changed, 1319 insertions(+), 1741 deletions(-) create mode 100644 backend_bot.py create mode 100644 backend_flask.py create mode 100644 backend_locks.py create mode 100644 backend_zabbix.py create mode 100644 bot_database.py create mode 100644 config.py create mode 100644 rabbitmq.py delete mode 100644 rabbitmq_worker.py create mode 100644 templates/index.html create mode 100644 utils.py create mode 100644 webui/__init__.py create mode 100644 webui/index.py delete mode 100644 zabbix_manager.py diff --git a/backend_bot.py b/backend_bot.py new file mode 100644 index 0000000..373ad3a --- /dev/null +++ b/backend_bot.py @@ -0,0 +1,177 @@ +import sqlite3 + +import telebot + +import telezab +from backend_locks import db_lock, bot +from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \ + get_user_subscribed_regions +from config import DB_PATH +from telezab import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button +from utils import show_main_menu, show_settings_menu + + +def handle_main_menu(message, chat_id, text): + """Обработка команд в главном меню.""" + if text == 'Регистрация': + telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + telezab.handle_register(message) + elif text == 'Настройки': + telezab.user_state_manager.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): + """Обработка команд в меню настроек.""" + admins_list = get_admins() + if text.lower() == 'подписаться': + telezab.user_state_manager.set_state(chat_id, "SUBSCRIBE") + handle_subscribe_button(message) + elif text.lower() == 'отписаться': + telezab.user_state_manager.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.user_state_manager.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.user_state_manager.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 = message.text.split(',') + valid_region_ids = [region[0] for region in get_sorted_regions()] + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + for region_id in region_ids: + region_id = region_id.strip() + if region_id not in valid_region_ids: + invalid_regions.append(region_id) + continue + cursor.execute( + 'INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username, active) VALUES (?, ?, ?, TRUE)', + (chat_id, region_id, username)) + if cursor.rowcount == 0: + cursor.execute('UPDATE subscriptions SET active = TRUE WHERE chat_id = ? AND region_id = ?', + (chat_id, region_id)) + subbed_regions.append(region_id) + conn.commit() + if len(invalid_regions) > 0: + bot.send_message(chat_id, + f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") + bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.") + log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}") + telezab.user_state_manager.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.user_state_manager.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.user_state_manager.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=f"cancel_action")) + if message.text.lower() == 'отмена': + bot.send_message(chat_id, "Действие отменено.") + telezab.user_state_manager.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 = message.text.split(',') + valid_region_ids = [region[0] for region in get_user_subscribed_regions(chat_id)] + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + for region_id in region_ids: + region_id = region_id.strip() + if region_id not in valid_region_ids: + invalid_regions.append(region_id) + continue + # Удаление подписки + query = 'UPDATE subscriptions SET active = FALSE WHERE chat_id = ? AND region_id = ?' + cursor.execute(query, (chat_id, region_id)) + unsubbed_regions.append(region_id) + conn.commit() + if len(invalid_regions) > 0: + 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.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + show_settings_menu(chat_id) diff --git a/backend_flask.py b/backend_flask.py new file mode 100644 index 0000000..a48cb3e --- /dev/null +++ b/backend_flask.py @@ -0,0 +1,348 @@ +import logging +import sqlite3 + +import telebot +from flask import Flask, request, jsonify, render_template + +import backend_bot +import bot_database +import telezab +import utils +from backend_locks import db_lock +from config import BASE_URL, DB_PATH +from utils import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email + +app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates') +# app.register_blueprint(webui) + +# Настройка уровня логирования для Flask +app.logger.setLevel(logging.INFO) + + +@app.route(BASE_URL + '/webhook', methods=['POST']) +def webhook(): + try: + # Получаем данные и логируем + data = request.get_json() + app.logger.info(f"Получены данные: {data}") + + # Генерация хеша события и логирование + event_hash = bot_database.hash_data(data) + app.logger.debug(f"Сгенерирован хеш для события: {event_hash}") + + # Работа с базой данных в блоке синхронизации + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Проверяем количество записей в таблице событий + cursor.execute('SELECT COUNT(*) FROM events') + count = cursor.fetchone()[0] + app.logger.debug(f"Текущее количество записей в таблице events: {count}") + + # Если записей >= 200, удаляем самое старое событие + if count >= 200: + query = 'DELETE FROM events WHERE id = (SELECT MIN(id) FROM events)' + app.logger.debug(f"Удаление старого события: {query}") + cursor.execute(query) + + # Извлечение номера региона из поля host + region_id = extract_region_number(data.get("host")) + if region_id is None: + app.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}") + return jsonify({"status": "error", "message": "Invalid host format"}), 400 + app.logger.debug(f"Извлечён номер региона: {region_id}") + + # Запрос подписчиков для отправки уведомления в зависимости от уровня критичности + if data['severity'] == 'Disaster': # Авария + query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE' + else: # Высокая критичность + query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE AND disaster_only = FALSE' + + app.logger.debug(f"Выполнение запроса: {query} для region_id={region_id}") + cursor.execute(query, (region_id,)) + results = cursor.fetchall() + + app.logger.debug(f"Найдено подписчиков: {len(results)} для региона {region_id}") + + # Проверка статуса региона (активен или нет) + query = 'SELECT active FROM regions WHERE region_id = ?' + cursor.execute(query, (region_id,)) + region_row = cursor.fetchone() + + if region_row and region_row[0]: # Если регион активен + app.logger.debug(f"Регион {region_id} активен. Начинаем рассылку сообщений.") + message = format_message(data) + undelivered = False + + # Отправляем сообщения подписчикам + for chat_id, username in results: + formatted_message = message.replace('\n', ' ').replace('\r', '') + + app.logger.info( + f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]") + try: + from rabbitmq import send_to_queue + send_to_queue({'chat_id': chat_id, 'username': username, 'message': message}) + app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})") + except Exception as e: + app.logger.error(f"Ошибка при отправке сообщения для {chat_id} (@{username}): {e}") + undelivered = True + + # Сохранение события, если были проблемы с доставкой + if undelivered: + query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)' + app.logger.debug( + f"Сохранение события в базе данных: {query} (hash={event_hash}, delivered={False})") + cursor.execute(query, (event_hash, str(data), False)) + + # Коммитим изменения в базе данных + conn.commit() + app.logger.debug("Изменения в базе данных успешно сохранены.") + conn.close() + + # Возвращаем успешный ответ + return jsonify({"status": "success"}), 200 + + except sqlite3.OperationalError as e: + app.logger.error(f"Ошибка операции с базой данных: {e}") + return jsonify({"status": "error", "message": "Ошибка работы с базой данных"}), 500 + + except ValueError as e: + app.logger.error(f"Ошибка значения: {e}") + return jsonify({"status": "error", "message": "Некорректные данные"}), 400 + + except Exception as e: + app.logger.error(f"Неожиданная ошибка: {e}") + return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500 + + +@app.route(BASE_URL + '/users/add', methods=['POST']) +def add_user(): + data = request.get_json() + + telegram_id = data.get('telegram_id') + chat_id = data.get('chat_id') + user_email = data.get('user_email') + + # DEBUG: Логирование полученных данных + app.logger.debug(f"Получены данные для добавления пользователя: {data}") + + # Валидация данных + if not validate_chat_id(chat_id): + app.logger.warning(f"Ошибка валидации: некорректный chat_id: {chat_id}") + return jsonify({"status": "failure", "reason": "Invalid data chat_id must be digit"}), 400 + + if not validate_telegram_id(telegram_id): + app.logger.warning(f"Ошибка валидации: некорректный telegram_id: {telegram_id}") + return jsonify({"status": "failure", "reason": "Invalid data telegram id must start from '@'"}), 400 + + if not validate_email(user_email): + app.logger.warning(f"Ошибка валидации: некорректный email: {user_email}") + return jsonify({"status": "failure", "reason": "Invalid data email address must be from rtmis"}), 400 + + if telegram_id and chat_id and user_email: + try: + # INFO: Попытка отправить сообщение пользователю + app.logger.info(f"Отправка сообщения пользователю {telegram_id} с chat_id {chat_id}") + backend_bot.bot.send_message(chat_id, "Регистрация пройдена успешно.") + # DEBUG: Попытка добавления пользователя в whitelist + app.logger.debug(f"Добавление пользователя {telegram_id} в whitelist") + success = bot_database.rundeck_add_to_whitelist(chat_id, telegram_id, user_email) + if success: + # INFO: Пользователь успешно добавлен в whitelist + app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.") + telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") + + # DEBUG: Показ основного меню пользователю + app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}") + utils.show_main_menu(chat_id) + return jsonify( + {"status": "success", "msg": f"User {telegram_id} with {user_email} added successfully"}), 200 + else: + # INFO: Пользователь уже существует в системе + app.logger.info(f"Пользователь с chat_id {chat_id} уже существует.") + return jsonify({"status": "failure", "msg": "User already exists"}), 400 + except telebot.apihelper.ApiTelegramException as e: + if e.result.status_code == 403: + # INFO: Пользователь заблокировал бота + app.logger.info(f"Пользователь {telegram_id} заблокировал бота") + return jsonify({"status": "failure", "msg": f"User {telegram_id} is blocked chat with bot"}) + elif e.result.status_code == 400: + # WARNING: Пользователь неизвестен боту, возможно не нажал /start + app.logger.warning( + f"Пользователь {telegram_id} с chat_id {chat_id} неизвестен боту, возможно, не нажал /start") + return jsonify({"status": "failure", + "msg": f"User {telegram_id} with {chat_id} is unknown to the bot, did the user press /start button?"}) + else: + # ERROR: Неизвестная ошибка при отправке сообщения + app.logger.error(f"Ошибка при отправке сообщения пользователю {telegram_id}: {str(e)}") + return jsonify({"status": "failure", "msg": f"{e}"}) + else: + # ERROR: Ошибка валидации — недостаточно данных + app.logger.error("Получены некорректные данные для добавления пользователя.") + return jsonify({"status": "failure", "reason": "Invalid data"}), 400 + + +@app.route(BASE_URL + '/users/del', methods=['POST']) +def delete_user(): + data = request.get_json() + user_email = data.get('email') + conn = sqlite3.connect(DB_PATH) + try: + # DEBUG: Получен запрос и начинается обработка + app.logger.debug(f"Получен запрос на удаление пользователя. Данные: {data}") + + if not user_email: + # WARNING: Ошибка валидации данных, email отсутствует + app.logger.warning(f"Ошибка валидации: отсутствует email") + return jsonify({"status": "failure", "message": "Email is required"}), 400 + + cursor = conn.cursor() + + # DEBUG: Запрос на получение chat_id + app.logger.debug(f"Выполняется запрос на получение chat_id для email: {user_email}") + cursor.execute("SELECT chat_id FROM whitelist WHERE user_email = ?", (user_email,)) + user = cursor.fetchone() + + if user is None: + # WARNING: Пользователь с указанным email не найден + app.logger.warning(f"Пользователь с email {user_email} не найден") + return jsonify({"status": "failure", "message": "User not found"}), 404 + chat_id = user[0] + + # INFO: Удаление пользователя и его подписок начато + app.logger.info(f"Начато удаление пользователя с email {user_email} и всех его подписок") + + # DEBUG: Удаление пользователя из whitelist + app.logger.debug(f"Удаление пользователя с email {user_email} из whitelist") + cursor.execute("DELETE FROM whitelist WHERE user_email = ?", (user_email,)) + + # DEBUG: Удаление подписок пользователя + app.logger.debug(f"Удаление подписок для пользователя с chat_id {chat_id}") + cursor.execute("DELETE FROM subscriptions WHERE chat_id = ?", (chat_id,)) + + conn.commit() + # INFO: Пользователь и подписки успешно удалены + app.logger.info(f"Пользователь с email {user_email} и все его подписки успешно удалены") + return jsonify( + {"status": "success", "message": f"User with email {user_email} and all subscriptions deleted."}), 200 + except Exception as e: + conn.rollback() + # ERROR: Ошибка при удалении данных + app.logger.error(f"Ошибка при удалении пользователя с email {user_email}: {str(e)}") + return jsonify({"status": "failure", "message": str(e)}), 500 + finally: + conn.close() + # DEBUG: Соединение с базой данных закрыто + app.logger.debug(f"Соединение с базой данных закрыто") + + +@app.route(BASE_URL + '/users/get', methods=['GET']) +def get_users(): + try: + # INFO: Запрос на получение списка пользователей + app.logger.info("Запрос на получение информации о пользователях получен") + + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # DEBUG: Запрос данных из таблицы whitelist + app.logger.debug("Запрос данных пользователей из таблицы whitelist") + cursor.execute('SELECT * FROM whitelist') + users = cursor.fetchall() + app.logger.debug("Формирование словаря пользователей") + users_dict = {user_id: {'id': user_id, 'username': username, 'email': email, 'events': [], 'worker': '', + 'subscriptions': []} + for user_id, username, email in users} + + # DEBUG: Запрос данных событий пользователей + app.logger.debug("Запрос событий пользователей из таблицы user_events") + cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') + events = cursor.fetchall() + + # DEBUG: Обработка событий и добавление их в словарь пользователей + for chat_id, username, action, timestamp in events: + if chat_id in users_dict: + event = {'type': action, 'date': timestamp} + if "Subscribed to region" in action: + region = action.split(": ")[-1] + event['region'] = region + users_dict[chat_id]['events'].append(event) + + # DEBUG: Запрос данных подписок пользователей + app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions") + cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') + subscriptions = cursor.fetchall() + + # DEBUG: Добавление подписок к пользователям + for chat_id, region_id in subscriptions: + if chat_id in users_dict: + users_dict[chat_id]['subscriptions'].append(str(region_id)) + + # INFO: Формирование результата + app.logger.info("Формирование результата для ответа") + result = [] + for user in users_dict.values(): + ordered_user = { + 'email': user['email'], + 'username': user['username'], + 'id': user['id'], + 'worker': user['worker'], + 'events': user['events'], + 'subscriptions': ', '.join(user['subscriptions']) + } + result.append(ordered_user) + + # INFO: Успешная отправка данных пользователей + app.logger.info("Информация о пользователях успешно отправлена") + return jsonify(result) + + except Exception as e: + # ERROR: Ошибка при получении информации о пользователях + app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}") + return jsonify({'status': 'error', 'message': str(e)}), 500 + + +@app.route(BASE_URL + '/users', methods=['GET']) +def view_users(): + return render_template('users.html') + + +@app.route(BASE_URL + '/debug/flask', methods=['POST']) +def toggle_flask_debug(): + try: + data = request.get_json() + level = data.get('level').upper() + if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: + return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400 + + log_level = getattr(logging, level, logging.DEBUG) + app.logger.setLevel(log_level) + + for handler in app.logger.handlers: + handler.setLevel(log_level) + + return jsonify({'status': 'success', 'level': level}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + + +@app.route(BASE_URL + '/debug/telebot', methods=['POST']) +def toggle_telebot_debug(): + try: + data = request.get_json() + level = data.get('level').upper() + if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: + return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400 + + log_level = getattr(logging, level, logging.DEBUG) + telebot.logger.setLevel(log_level) + + for handler in telebot.logger.handlers: + handler.setLevel(log_level) + + return jsonify({'status': 'success', 'level': level}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 diff --git a/backend_locks.py b/backend_locks.py new file mode 100644 index 0000000..0da9c46 --- /dev/null +++ b/backend_locks.py @@ -0,0 +1,9 @@ +import threading + +import telebot + +db_lock = threading.Lock() +# bot_instance.py +from config import TOKEN + +bot = telebot.TeleBot(TOKEN) diff --git a/backend_zabbix.py b/backend_zabbix.py new file mode 100644 index 0000000..4ad60cf --- /dev/null +++ b/backend_zabbix.py @@ -0,0 +1,128 @@ +import re +import time +from datetime import datetime + +import telebot +from pytz import timezone +from pyzabbix import ZabbixAPI + +import backend_bot +from config import ZABBIX_URL, ZABBIX_API_TOKEN +from utils import show_main_menu + + +def get_triggers_for_group(chat_id, group_id): + triggers = get_zabbix_triggers(group_id) # Получаем все активные события без периода + if not triggers: + backend_bot.bot.send_message(chat_id, f"Нет активных событий.") + show_main_menu(chat_id) + else: + send_triggers_to_user(triggers, chat_id) + + +def get_triggers_for_all_groups(chat_id, region_id): + 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()] + + all_triggers = [] + for group in filtered_groups: + triggers = get_zabbix_triggers(group['groupid']) + if triggers: + all_triggers.extend(triggers) + + if all_triggers: + send_triggers_to_user(all_triggers, chat_id) + else: + backend_bot.bot.send_message(chat_id, f"Нет активных событий.") + show_main_menu(chat_id) + except Exception as e: + backend_bot.bot.send_message(chat_id, f"Ошибка при получении событий.\n{str(e)}") + 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): + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + telebot.logger.info(f"Fetching active hosts for group {group_id}") + + # Получаем список активных хостов в группе + active_hosts = zapi.host.get( + groupids=group_id, + output=["hostid", "name"], + filter={"status": "0"} # Только включенные хосты + ) + + if not active_hosts: + telebot.logger.info(f"No active hosts found for group {group_id}") + return [] + + host_ids = [host["hostid"] for host in active_hosts] + telebot.logger.info(f"Found {len(host_ids)} active hosts in group {group_id}") + + # Получение активных проблем для этих хостов + problems = zapi.problem.get( + output=["eventid", "name", "severity", "clock"], + hostids=host_ids, + suppressed=0, + acknowledged=0, + filter={"severity": ["4", "5"]}, # Только высокий и аварийный уровень + sortorder="ASC" + ) + + if not problems: + telebot.logger.info(f"No active problems found for group {group_id}") + return [] + + # Получение IP-адресов хостов + host_interfaces = zapi.hostinterface.get( + hostids=host_ids, + output=["hostid", "ip"] + ) + host_ip_map = {iface["hostid"]: iface["ip"] for iface in host_interfaces} + # print(host_ip_map) + moscow_tz = timezone('Europe/Moscow') + severity_map = {'4': 'HIGH', '5': 'DISASTER'} + priority_map = {'4': '⚠️', '5': '⛔️'} + problem_messages = [] + + for problem in problems: + event_time_epoch = int(problem['clock']) + event_time = datetime.fromtimestamp(event_time_epoch, tz=moscow_tz) + event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') + + severity = severity_map.get(problem['severity'], 'Неизвестно') + priority = priority_map.get(problem['severity'], '') + description = problem.get('name', 'Нет описания') + + # Получаем хост из описания (или по-другому, если известно) + host = extract_host_from_name(description) + host_ip = host_ip_map.get(problem.get("hostid"), "Неизвестный IP") + + message = (f"{priority} Host: {host}\n" + f"IP: {host_ip}\n" + f"Описание: {description}\n" + f"Критичность: {severity}\n" + f"Время создания: {event_time_formatted}") + + problem_messages.append(message) + + return problem_messages + except Exception as e: + telebot.logger.error(f"Error fetching problems for group {group_id}: {e}") + return None diff --git a/bot_database.py b/bot_database.py new file mode 100644 index 0000000..5ddfa76 --- /dev/null +++ b/bot_database.py @@ -0,0 +1,238 @@ +import hashlib +import os +import sqlite3 +import time +from threading import Lock + +import telebot + +from backend_flask import app +from config import DB_PATH + +# Lock for database operations +db_lock = Lock() + + +def init_db(): + try: + # 1️⃣ Проверяем и создаём каталог, если его нет + db_dir = os.path.dirname(DB_PATH) + if not os.path.exists(db_dir): + os.makedirs(db_dir, exist_ok=True) # Создаём каталог рекурсивно + + # 2️⃣ Проверяем, существует ли файл базы данных + db_exists = os.path.exists(DB_PATH) + + # 3️⃣ Открываем соединение, если файла нет, он создастся автоматически + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # 4️⃣ Если базы не было, создаём таблицы + if not db_exists: + cursor.execute('''CREATE TABLE events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT UNIQUE, + data TEXT, + delivered BOOLEAN)''') + + cursor.execute('''CREATE TABLE subscriptions ( + chat_id INTEGER, + region_id TEXT, + username TEXT, + active BOOLEAN DEFAULT TRUE, + skip BOOLEAN DEFAULT FALSE, + disaster_only BOOLEAN DEFAULT FALSE, + UNIQUE(chat_id, region_id))''') + + cursor.execute('''CREATE TABLE whitelist ( + chat_id INTEGER PRIMARY KEY, + username TEXT, + user_email TEXT)''') + + cursor.execute('''CREATE TABLE admins ( + chat_id INTEGER PRIMARY KEY, + username TEXT)''') + + cursor.execute('''CREATE TABLE regions ( + region_id TEXT PRIMARY KEY, + region_name TEXT, + active BOOLEAN DEFAULT TRUE)''') + + cursor.execute('''CREATE TABLE user_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id INTEGER, + username TEXT, + action TEXT, + timestamp TEXT)''') + + # Добавляем тестовые данные (если их нет) + cursor.execute('''INSERT OR IGNORE INTO regions (region_id, region_name) VALUES + ('01', 'Адыгея'), + ('02', 'Башкортостан (Уфа)'), + ('04', 'Алтай'), + ('19', 'Республика Хакасия')''') + + conn.commit() + app.logger.info("✅ Database created and initialized successfully.") + else: + app.logger.info("✅ Database already exists. Skipping initialization.") + + except Exception as e: + app.logger.error(f"❌ Error initializing database: {e}") + finally: + if 'conn' in locals(): # Проверяем, была ли создана переменная conn + conn.close() + + +def hash_data(data): + return hashlib.sha256(str(data).encode('utf-8')).hexdigest() + + +def is_whitelisted(chat_id): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' + telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}") + cursor.execute(query, (chat_id,)) + count = cursor.fetchone()[0] + conn.close() + return count > 0 + + +def add_to_whitelist(chat_id, username): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + query = 'INSERT OR IGNORE INTO whitelist (chat_id, username) VALUES (?, ?)' + telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}, username={username}") + try: + cursor.execute(query, (chat_id, username)) + conn.commit() + except Exception as e: + telebot.logger.error(f"Error during add to whitelist: {e}") + finally: + conn.close() + + +def rundeck_add_to_whitelist(chat_id, username, user_email): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Проверка существования chat_id + check_query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' + cursor.execute(check_query, (chat_id,)) + count = cursor.fetchone()[0] + + if count > 0: + conn.close() + return False # Пользователь уже существует + + # Вставка нового пользователя + insert_query = 'INSERT INTO whitelist (chat_id, username, user_email) VALUES (?, ?, ?)' + telebot.logger.info( + f"Rundeck executing query: {insert_query} with chat_id={chat_id}, username={username}, email={user_email}") + cursor.execute(insert_query, (chat_id, username, user_email)) + conn.commit() + conn.close() + return True # Успешное добавление + + +def remove_from_whitelist(chat_id): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + query = 'DELETE FROM whitelist WHERE chat_id = ?' + telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}") + cursor.execute(query, (chat_id,)) + conn.commit() + conn.close() + + +def get_admins(): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute('SELECT chat_id FROM admins') + admins = cursor.fetchall() + admins = [i[0] for i in admins] + conn.close() + return admins + + +def get_sorted_regions(): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute('SELECT region_id, region_name FROM regions WHERE active = TRUE') + regions = cursor.fetchall() + conn.close() + # Сортируем регионы по числовому значению region_id + regions.sort(key=lambda x: int(x[0])) + return regions + + +def region_exists(region_id): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ? AND active = TRUE', (region_id,)) + count = cursor.fetchone()[0] + conn.close() + return count > 0 + + +def get_user_subscribed_regions(chat_id): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT regions.region_id, regions.region_name + FROM subscriptions + JOIN regions ON subscriptions.region_id = regions.region_id + WHERE subscriptions.chat_id = ? AND subscriptions.active = TRUE AND subscriptions.skip = FALSE + ORDER BY regions.region_id + ''', (chat_id,)) + regions = cursor.fetchall() + conn.close() + # Сортируем регионы по числовому значению region_id + regions.sort(key=lambda x: int(x[0])) + return regions + + +def is_subscribed(chat_id, region_id): + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(''' + SELECT COUNT(*) + FROM subscriptions + WHERE chat_id = ? AND region_id = ? AND active = TRUE AND skip = FALSE + ''', (chat_id, region_id)) + count = cursor.fetchone()[0] + conn.close() + return count > 0 + + +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): + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + try: + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)' + telebot.logger.debug( + f"Executing query: {query} with chat_id={chat_id}, username={username}, action={action}, timestamp={timestamp}") + cursor.execute(query, (chat_id, username, action, timestamp)) + conn.commit() + telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {timestamp}.") + except Exception as e: + telebot.logger.error(f"Error logging user event: {e}") + finally: + conn.close() diff --git a/config.py b/config.py new file mode 100644 index 0000000..d77fc79 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +# load_dotenv() +import os + +DEV = os.getenv('DEV') +TOKEN = os.getenv('TELEGRAM_TOKEN') +ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') +ZABBIX_URL = os.getenv('ZABBIX_URL') +DB_PATH = 'db/telezab.db' +SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" +BASE_URL = '/telezab' +RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') +RABBITMQ_QUEUE = 'telegram_notifications' +RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') +RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') +RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" diff --git a/log_manager.py b/log_manager.py index 708d373..908003b 100644 --- a/log_manager.py +++ b/log_manager.py @@ -1,9 +1,10 @@ import logging -from logging.config import dictConfig -from logging.handlers import TimedRotatingFileHandler import os import zipfile from datetime import datetime, timedelta +from logging.config import dictConfig +from logging.handlers import TimedRotatingFileHandler + class UTF8StreamHandler(logging.StreamHandler): def __init__(self, stream=None): @@ -15,11 +16,13 @@ class UTF8StreamHandler(logging.StreamHandler): 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, log_dir='logs', retention_days=30): self.log_dir = log_dir @@ -166,7 +169,8 @@ class LogManager: werkzeug_logger.handlers = [] # Удаляем существующие обработчики # Добавляем кастомный обработчик для форматирования логов - handler = TimedRotatingFileHandler(self.log_files['flask'], when='midnight', backupCount=self.retention_days, encoding='utf-8') + handler = TimedRotatingFileHandler(self.log_files['flask'], when='midnight', backupCount=self.retention_days, + encoding='utf-8') handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s %(message)s')) werkzeug_logger.addHandler(handler) @@ -198,4 +202,3 @@ class LogManager: """Rotates and archives logs.""" self.archive_old_logs() self.schedule_log_rotation() # Schedule the next rotation - diff --git a/rabbitmq.py b/rabbitmq.py new file mode 100644 index 0000000..a45382c --- /dev/null +++ b/rabbitmq.py @@ -0,0 +1,155 @@ +import asyncio +import json +from concurrent.futures import ThreadPoolExecutor +from functools import partial + +import aio_pika +import aiohttp +import pika +import telebot + +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, # Интервал heartbeat для поддержания соединения + 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, # make message persistent + )) + connection.close() + + +async def consume_from_queue(): + while True: # Бесконечный цикл для переподключения + try: + # Подключение к RabbitMQ + 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) + + # Проверка структуры сообщения + if not isinstance(data, dict): + raise ValueError("Invalid message format: Expected a dictionary") + + # Извлечение необходимых данных + chat_id = data.get("chat_id") + username = data.get("username") + message_text = data.get("message") + + # Проверка обязательных полей + if not all([chat_id, username, message_text]): + raise ValueError(f"Missing required fields in message: {data}") + + # Отправляем сообщение + await send_notification_message(chat_id, message_text, username) + except json.JSONDecodeError: + # Логируем некорректный JSON + telebot.logger.error(f"Failed to decode message: {message.body}") + except ValueError as ve: + # Логируем ошибку формата сообщения + telebot.logger.error(f"Invalid message: {ve}") + except Exception as e: + # Логируем общую ошибку при обработке + telebot.logger.error(f"Error sending message from queue: {e}") + except aio_pika.exceptions.AMQPError as e: + # Логируем ошибку RabbitMQ и переподключаемся + telebot.logger.error(f"RabbitMQ error: {e}") + except Exception as e: + # Логируем общую ошибку и ждем перед переподключением + telebot.logger.error(f"Critical error in consume_from_queue: {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() + parse_mode = 'HTML' + + # Используем partial для передачи именованных аргументов в bot.send_message + func_with_args = partial(backend_bot.bot.send_message, chat_id=chat_id, text=message, parse_mode=parse_mode) + + # Передаем подготовленную функцию в run_in_executor + await run_in_executor(func_with_args) + + except telebot.apihelper.ApiTelegramException as e: + if "429" in str(e): + telebot.logger.warning(f"Rate limit exceeded for chat_id {chat_id}. Retrying...") + await asyncio.sleep(1) + await send_message(chat_id, message, is_notification) + elif "403" in str(e): + telebot.logger.warning(f"Can't send message to user because bot blocked by user with chat id: {chat_id}") + pass + else: + telebot.logger.error(f"Failed to send message to {chat_id}: {e}") + telebot.logger.error(f"Detailed Error: {e}", exc_info=True) # Добавлено логирование исключения + except Exception as e: + username = f"@{message.from_user.username}" if message.from_user.username else "N/A" + telebot.logger.error(f"Unexpected error while sending message to {username} {chat_id}: {e}", exc_info=True) + await check_telegram_api() + finally: + if is_notification: + rate_limit_semaphore.release() + formatted_message = message.replace('\n', ' ').replace('\r', '') + telebot.logger.info(f'Send notification to {chat_id} from RabbitMQ [{formatted_message}]') + + +async def send_notification_message(chat_id, message, username): + await send_message(chat_id, message, is_notification=True) + + +async def run_in_executor(func, *args): + loop = asyncio.get_event_loop() + with ThreadPoolExecutor() as pool: + return await loop.run_in_executor(pool, func, *args) + + +async def check_telegram_api(): + try: + async with aiohttp.ClientSession() as session: + async with session.get('https://api.telegram.org') as response: + if response.status == 200: + telebot.logger.info("Telegram API is reachable.") + else: + telebot.logger.error("Telegram API is not reachable.") + except Exception as e: + telebot.logger.error(f"Error checking Telegram API: {e}") diff --git a/rabbitmq_worker.py b/rabbitmq_worker.py deleted file mode 100644 index 43b3c36..0000000 --- a/rabbitmq_worker.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -import asyncio -import logging -import json -import time -import pika -from concurrent.futures import ThreadPoolExecutor -from functools import partial - -# RabbitMQ configuration -RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost') -RABBITMQ_QUEUE = 'telegram_notifications' -RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') -RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') - -# Импорт функций для отправки сообщений -from telezab import send_notification_message # Замените на актуальный путь к send_notification_message - - -class RabbitMQWorker: - def __init__(self): - self.connection = None - self.channel = None - - def rabbitmq_connection(self): - """Устанавливает подключение к RabbitMQ.""" - try: - credentials = pika.PlainCredentials(RABBITMQ_LOGIN, RABBITMQ_PASS) - parameters = pika.ConnectionParameters( - host=RABBITMQ_HOST, - 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) - logging.info("RabbitMQ connection established.") - except Exception as e: - logging.error(f"Error establishing RabbitMQ connection: {e}") - self.connection = None - self.channel = None - - async def consume_from_queue(self): - """Основной цикл обработки сообщений из RabbitMQ.""" - while True: - try: - if not self.connection or self.connection.is_closed: - self.rabbitmq_connection() - - for method_frame, properties, body in self.channel.consume(RABBITMQ_QUEUE, inactivity_timeout=5): - if not method_frame: - continue # Нет новых сообщений, продолжаем ждать - - # Декодируем сообщение из очереди - message = json.loads(body) - chat_id = message['chat_id'] - username = message['username'] - message_text = message['message'] - - try: - # Отправляем сообщение - await send_notification_message(chat_id, message_text, username) - self.channel.basic_ack(method_frame.delivery_tag) # Подтверждаем получение - except Exception as e: - logging.error(f"Error processing message: {e}") - self.channel.basic_nack(method_frame.delivery_tag) # Возвращаем сообщение в очередь - - except pika.exceptions.AMQPConnectionError as e: - logging.error(f"RabbitMQ connection error: {e}. Reconnecting in 5 seconds...") - self.close_connection() - await asyncio.sleep(5) - except Exception as e: - logging.error(f"Unexpected error in RabbitMQ consumer: {e}") - await asyncio.sleep(5) - - def close_connection(self): - """Закрывает соединение с RabbitMQ.""" - if self.connection and not self.connection.is_closed: - self.connection.close() - logging.info("RabbitMQ connection closed.") - - async def run(self): - """Запускает основной цикл обработки очереди.""" - try: - await self.consume_from_queue() - except asyncio.CancelledError: - logging.info("RabbitMQ consumer stopped.") - finally: - self.close_connection() diff --git a/requirements.txt b/requirements.txt index aeb851bd446b549b7d6adff7102b4f37bfe9225c..dcf90c6af6db011a3d5e8dc120bb7e200d1a39fa 100644 GIT binary patch literal 567 zcmXw0L5|}f5WMr3AY;c-a^Ns?7-?4;X{23g&uvVc9upvdSFvBeg`>UEUENh(1#vh; zo=NMA?B!7qha1JX+SLx^T-LgkM|s-&r9`besgzW0ZKV4{>ytc7RjbUCC#SWNodjG; zs>{7pvJr!0KigEn3rb^C;VT1f_JK|bbEA}PIe4&=lEE z+Sht7J}J#h{6$w=I}OIl=?p8vJB8_ zu-b{3LNsJZ GjN%_I#HnTg literal 972 zcmYL{PfNp45XAQ^_)#h~t*stB=s{2r1O?COzcku5wMoVH!>hm9&1(n`lFaPv?7aQ^ z&8@V`me%lW?UrX?7k0zBvU5ALz-w5AZTMO_=Mr4J%a)_+oz;$Ij%1*HM<>*j)PC9= zjykCNnv*#`_5Tf=1xI*BWoAPdW6rQ;E$OZvb9(jAtN2>GqtcEiZMo7(+Uh-}w{+E0 zy~nL$g)^z-t|LYtRDDOzcmU7o7}Zi0K5%S7UFCf@K3|QiG@BG6nbKySnh@1T=MruW zl7kiQyt4f%J$X;rJ$Z5;Ff&(|h9-zip83DS-JY9!<$ZycI;mc)xWP5}ijMN4EW+mq zT+;++OSMwU@uRycTAgl^E&qk58M}-V(qn>#UXykbsmn#=6?@dfX{QrgmZsU-!QPl) z#VOibxhLbs?(D@Ly|eset{S_gnuhJI@Z4iQvFE&hpL~W3ZE|pr?PO`g;+}*ZnQp>U z+Rv#v@>cZ@-7lt_`;=+ZHJ#Fq8EZ0J!kBsDaTiKJjM&FLIxF9o2{q}iI{ck#-Kh3? U-`;{ebbEKer8Lq#D8tx>f8M%_F#rGn diff --git a/telezab.py b/telezab.py index 76bb25c..dc4233f 100644 --- a/telezab.py +++ b/telezab.py @@ -1,315 +1,48 @@ -import os -from functools import partial -from flask import Flask, request, jsonify, render_template -from dotenv import load_dotenv -import hashlib -import telebot -from telebot import types -import logging -from threading import Thread, Lock -import sqlite3 -import time import asyncio -import aiohttp -import pika -import aio_pika -import json -from concurrent.futures import ThreadPoolExecutor +import logging +import sqlite3 +from threading import Thread + +import telebot from pyzabbix import ZabbixAPI -import requests -from pytz import timezone -from datetime import datetime -import re -import urllib.parse +from telebot import types +import backend_bot +import bot_database +from backend_flask import app +from backend_locks import bot +from backend_locks import db_lock +from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups +from config import * from log_manager import LogManager -# from rabbitmq_worker import RABBITMQ_LOGIN -from region_api import RegionAPI +from rabbitmq import consume_from_queue from user_state_manager import UserStateManager -# Load environment variables -load_dotenv() +from utils import show_main_menu, show_settings_menu - -DEV = os.getenv('DEV') -# Загрузка переменных окружения или значений из файлов -TOKEN = os.getenv('TELEGRAM_TOKEN') -ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') -ZABBIX_URL = os.getenv('ZABBIX_URL') -DB_PATH = 'db/telezab.db' -SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" -BASE_URL = '/telezab' - -# RabbitMQ configuration -RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost') -RABBITMQ_QUEUE = 'telegram_notifications' -RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') -RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') -RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" -# Инициализируем класс RegionApi -region_api = RegionAPI(DB_PATH) # Инициализируем класс UserStateManager user_state_manager = UserStateManager() -# Initialize Flask application -app = Flask(__name__,static_url_path='/telezab/static', template_folder='templates') - - # Инициализация LogManager log_manager = LogManager(log_dir='logs', retention_days=30) -# Настройка уровня логирования для Flask -app.logger.setLevel(logging.INFO) - # Настройка pyTelegramBotAPI logger telebot.logger = logging.getLogger('telebot') # Важно: вызов schedule_log_rotation для планировки ротации и архивации логов log_manager.schedule_log_rotation() -bot = telebot.TeleBot(TOKEN) -# Lock for database operations -db_lock = Lock() +# # Lock for database operations +# db_lock = Lock() -# Semaphore for rate limiting -rate_limit_semaphore = asyncio.Semaphore(25) # 25 messages per second - - -def init_db(): - try: - # 1️⃣ Проверяем и создаём каталог, если его нет - db_dir = os.path.dirname(DB_PATH) - if not os.path.exists(db_dir): - os.makedirs(db_dir, exist_ok=True) # Создаём каталог рекурсивно - - # 2️⃣ Проверяем, существует ли файл базы данных - db_exists = os.path.exists(DB_PATH) - - # 3️⃣ Открываем соединение, если файла нет, он создастся автоматически - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # 4️⃣ Если базы не было, создаём таблицы - if not db_exists: - cursor.execute('''CREATE TABLE events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - hash TEXT UNIQUE, - data TEXT, - delivered BOOLEAN)''') - - cursor.execute('''CREATE TABLE subscriptions ( - chat_id INTEGER, - region_id TEXT, - username TEXT, - active BOOLEAN DEFAULT TRUE, - skip BOOLEAN DEFAULT FALSE, - disaster_only BOOLEAN DEFAULT FALSE, - UNIQUE(chat_id, region_id))''') - - cursor.execute('''CREATE TABLE whitelist ( - chat_id INTEGER PRIMARY KEY, - username TEXT, - user_email TEXT)''') - - cursor.execute('''CREATE TABLE admins ( - chat_id INTEGER PRIMARY KEY, - username TEXT)''') - - cursor.execute('''CREATE TABLE regions ( - region_id TEXT PRIMARY KEY, - region_name TEXT, - active BOOLEAN DEFAULT TRUE)''') - - cursor.execute('''CREATE TABLE user_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - chat_id INTEGER, - username TEXT, - action TEXT, - timestamp TEXT)''') - - # Добавляем тестовые данные (если их нет) - cursor.execute('''INSERT OR IGNORE INTO regions (region_id, region_name) VALUES - ('01', 'Адыгея'), - ('02', 'Башкортостан (Уфа)'), - ('04', 'Алтай'), - ('19', 'Республика Хакасия')''') - - conn.commit() - app.logger.info("✅ Database created and initialized successfully.") - else: - app.logger.info("✅ Database already exists. Skipping initialization.") - - except Exception as e: - app.logger.error(f"❌ Error initializing database: {e}") - finally: - if 'conn' in locals(): # Проверяем, была ли создана переменная conn - conn.close() - - - -# Hash the incoming data -def hash_data(data): - return hashlib.sha256(str(data).encode('utf-8')).hexdigest() - - -# Check if user is in whitelist -def is_whitelisted(chat_id): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' - telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}") - cursor.execute(query, (chat_id,)) - count = cursor.fetchone()[0] - conn.close() - return count > 0 - - -# Add user to whitelist -def add_to_whitelist(chat_id, username): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - query = 'INSERT OR IGNORE INTO whitelist (chat_id, username) VALUES (?, ?)' - telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}, username={username}") - try: - cursor.execute(query, (chat_id, username)) - conn.commit() - except Exception as e: - telebot.logger.error(f"Error during add to whitelist: {e}") - finally: - conn.close() - -def rundeck_add_to_whitelist(chat_id, username, user_email): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Проверка существования chat_id - check_query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' - cursor.execute(check_query, (chat_id,)) - count = cursor.fetchone()[0] - - if count > 0: - conn.close() - return False # Пользователь уже существует - - # Вставка нового пользователя - insert_query = 'INSERT INTO whitelist (chat_id, username, user_email) VALUES (?, ?, ?)' - telebot.logger.info(f"Rundeck executing query: {insert_query} with chat_id={chat_id}, username={username}, email={user_email}") - cursor.execute(insert_query, (chat_id, username, user_email)) - conn.commit() - conn.close() - return True # Успешное добавление - - -# Remove user from whitelist -def remove_from_whitelist(chat_id): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - query = 'DELETE FROM whitelist WHERE chat_id = ?' - telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}") - cursor.execute(query, (chat_id,)) - conn.commit() - conn.close() - -def get_admins(): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT chat_id FROM admins') - admins = cursor.fetchall() - admins = [i[0] for i in admins] - conn.close() - return admins - - -def get_sorted_regions(): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT region_id, region_name FROM regions WHERE active = TRUE') - regions = cursor.fetchall() - conn.close() - # Сортируем регионы по числовому значению region_id - regions.sort(key=lambda x: int(x[0])) - return regions - -# Check if region exists -def region_exists(region_id): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ? AND active = TRUE', (region_id,)) - count = cursor.fetchone()[0] - conn.close() - return count > 0 - -# Get list of regions a user is subscribed to -def get_user_subscribed_regions(chat_id): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(''' - SELECT regions.region_id, regions.region_name - FROM subscriptions - JOIN regions ON subscriptions.region_id = regions.region_id - WHERE subscriptions.chat_id = ? AND subscriptions.active = TRUE AND subscriptions.skip = FALSE - ORDER BY regions.region_id - ''', (chat_id,)) - regions = cursor.fetchall() - conn.close() - # Сортируем регионы по числовому значению region_id - regions.sort(key=lambda x: int(x[0])) - return regions - -# Check if user is subscribed to a region -def is_subscribed(chat_id, region_id): - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - cursor.execute(''' - SELECT COUNT(*) - FROM subscriptions - WHERE chat_id = ? AND region_id = ? AND active = TRUE AND skip = FALSE - ''', (chat_id, region_id)) - count = cursor.fetchone()[0] - conn.close() - return count > 0 - -# Format regions list -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): - timestamp = time.strftime('%Y-%m-%d %H:%M:%S') - try: - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)' - telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, username={username}, action={action}, timestamp={timestamp}") - cursor.execute(query, (chat_id, username, action, timestamp)) - conn.commit() - telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {timestamp}.") - except Exception as e: - telebot.logger.error(f"Error logging user event: {e}") - finally: - conn.close() - -# Define states -NOTIFICATION_MODE = 1 -SETTINGS_MODE = 2 +# 25 messages per second # Handle /help command to provide instructions @bot.message_handler(commands=['help']) def handle_help(message): chat_id = message.chat.id - if not is_whitelisted(chat_id): - bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.") + if not bot_database.is_whitelisted(chat_id): + backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.") return help_text = ( '/start - Показать меню бота\n' @@ -317,9 +50,10 @@ def handle_help(message): 'Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n' 'Помощь - Описание всех возможностей бота' ) - bot.send_message(message.chat.id, help_text, parse_mode="html") + 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 @@ -328,14 +62,16 @@ def handle_register(message): username = f"@{username}" else: username = "N/A" - text = (f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n' - f'В теме письма указать "Подтверждение регистрации в телеграм-боте TeleZab".\n' - f'В теле письма указать:\n' - f'1. ФИО\n' - f'2. Ваш Chat ID: {chat_id}\n' - f'3. Ваше имя пользователя: {username}') - bot.send_message(chat_id,text,parse_mode="HTML") - log_user_event(chat_id, username, "Requested registration") + text = ( + f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n' + f'В теме письма указать "Подтверждение регистрации в телеграм-боте TeleZab".\n' + f'В теле письма указать:\n' + f'1. ФИО\n' + f'2. Ваш Chat ID: {chat_id}\n' + f'3. Ваше имя пользователя: {username}') + backend_bot.bot.send_message(chat_id, text, parse_mode="HTML") + bot_database.log_user_event(chat_id, username, "Requested registration") + # Handle /start command @bot.message_handler(commands=['start']) @@ -343,38 +79,7 @@ def handle_start(message): show_main_menu(message.chat.id) -def show_main_menu(chat_id): - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - if is_whitelisted(chat_id): - user_state_manager.set_state(chat_id, "MAIN_MENU") - markup.add('Настройки', 'Помощь', 'Активные события') - else: - user_state_manager.set_state(chat_id, "REGISTRATION") - markup.add('Регистрация') - bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) - - -def create_settings_keyboard(chat_id, admins_list): - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - # Линия 1: "Подписаться", "Отписаться" - markup.row('Подписаться','Отписаться') - markup.row('Мои подписки','Режим уведомлений') - if DEV == '1': - if chat_id in admins_list: - markup.row('Активные регионы') - markup.row('Добавить регион', 'Удалить регион') - markup.row('Назад') - return markup - # Settings menu for users -def show_settings_menu(chat_id): - if not is_whitelisted(chat_id): - user_state_manager.set_state(chat_id, "REGISTRATION") - bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") - return - admins_list = get_admins() - markup = create_settings_keyboard(chat_id, admins_list) - bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup) # Основной обработчик меню @bot.message_handler(func=lambda message: True) @@ -383,204 +88,34 @@ def handle_menu_selection(message): text = message.text.strip() username = message.from_user.username # Проверка авторизации - if not is_whitelisted(chat_id) and text != 'Регистрация': - bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + if not bot_database.is_whitelisted(chat_id) and text != 'Регистрация': + backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") return # Получаем текущее состояние пользователя current_state = user_state_manager.get_state(chat_id) # Обработка команд в зависимости от состояния if current_state == "MAIN_MENU": - handle_main_menu(message, chat_id, text) + backend_bot.handle_main_menu(message, chat_id, text) elif current_state == "REGISTRATION": handle_register(message) elif current_state == "SETTINGS_MENU": - handle_settings_menu(message, chat_id, text) + backend_bot.handle_settings_menu(message, chat_id, text) elif current_state == "SUBSCRIBE": - process_subscription_button(message, chat_id, username) + backend_bot.process_subscription_button(message, chat_id, username) elif current_state == "UNSUBSCRIBE": - process_unsubscription_button(message, chat_id, username) - # elif current_state == "ADD_REGION": - # process_add_region_button(message, chat_id, username) - # elif current_state == "REMOVE_REGION": - # process_remove_region_button(message, chat_id, username) + backend_bot.process_unsubscription_button(message, chat_id, username) else: - bot.send_message(chat_id, "Команда не распознана.") + backend_bot.bot.send_message(chat_id, "Команда не распознана.") show_main_menu(chat_id) -def handle_main_menu(message, chat_id, text): - """Обработка команд в главном меню.""" - if text == 'Регистрация': - user_state_manager.set_state(chat_id, "REGISTRATION") - handle_register(message) - elif text == 'Настройки': - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - elif text == 'Помощь': - handle_help(message) - elif text == 'Активные события': - handle_active_triggers(message) - else: - bot.send_message(chat_id, "Команда не распознана.") - show_main_menu(chat_id) - - -def handle_settings_menu(message, chat_id, text): - """Обработка команд в меню настроек.""" - admins_list = get_admins() - if text.lower() == 'подписаться': - user_state_manager.set_state(chat_id, "SUBSCRIBE") - handle_subscribe_button(message) - elif text.lower() == 'отписаться': - user_state_manager.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() == 'добавить регион' and chat_id in admins_list: - user_state_manager.set_state(chat_id, "ADD_REGION") - handle_region_manager(chat_id, 'add') - elif text.lower() == 'удалить регион' and chat_id in admins_list: - user_state_manager.set_state(chat_id, "REMOVE_REGION") - handle_region_manager(chat_id, 'remove') - elif text.lower() == 'назад': - user_state_manager.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, "Действие отменено.") - user_state_manager.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 = message.text.split(',') - valid_region_ids = [region[0] for region in get_sorted_regions()] - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - for region_id in region_ids: - region_id = region_id.strip() - if region_id not in valid_region_ids: - invalid_regions.append(region_id) - continue - cursor.execute('INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username, active) VALUES (?, ?, ?, TRUE)', - (chat_id, region_id, username)) - if cursor.rowcount == 0: - cursor.execute('UPDATE subscriptions SET active = TRUE WHERE chat_id = ? AND region_id = ?', (chat_id, region_id)) - subbed_regions.append(region_id) - conn.commit() - if len(invalid_regions) > 0: - bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") - bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.") - log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}") - user_state_manager.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}") - user_state_manager.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, "Вы не подписаны ни на один регион.") - user_state_manager.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=f"cancel_action")) - if message.text.lower() == 'отмена': - bot.send_message(chat_id, "Действие отменено.") - user_state_manager.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 = message.text.split(',') - valid_region_ids = [region[0] for region in get_user_subscribed_regions(chat_id)] - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - for region_id in region_ids: - region_id = region_id.strip() - if region_id not in valid_region_ids: - invalid_regions.append(region_id) - continue - # Удаление подписки - query = 'UPDATE subscriptions SET active = FALSE WHERE chat_id = ? AND region_id = ?' - cursor.execute(query, (chat_id, region_id)) - unsubbed_regions.append(region_id) - conn.commit() - if len(invalid_regions) > 0: - 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)}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - @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 - bot.clear_step_handler_by_chat_id(chat_id) - bot.send_message(chat_id,f"Действие отменено") - bot.edit_message_reply_markup(chat_id,message_id,reply_markup=None) + 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) user_state_manager.set_state(chat_id, "SETTINGS_MENU") show_settings_menu(chat_id) return @@ -590,203 +125,31 @@ def handle_cancel_action(call): def handle_cancel_active_triggers(call): chat_id = call.message.chat.id message_id = call.message.message_id - bot.clear_step_handler_by_chat_id(chat_id) - bot.send_message(chat_id,f"Действие отменено") - bot.edit_message_reply_markup(chat_id,message_id,reply_markup=None) + 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) user_state_manager.set_state(chat_id, "MAIN_MENU") show_main_menu(chat_id) return -###################################################################################################################### -# Handle admin region management commands -###################################################################################################################### -def handle_region_manager(chat_id: int, action: str): - if action == 'add': - bot.send_message(chat_id, "Введите ID и название региона в формате:\n ") - bot.register_next_step_handler_by_chat_id(chat_id, process_add_region) - elif action == 'remove': - bot.send_message(chat_id, "Введите ID региона, который хотите сделать неактивным") - bot.register_next_step_handler_by_chat_id(chat_id, process_remove_region) -###################################################################################################################### -# Handle admin region management commands -###################################################################################################################### -class RegionManager: - def __init__(self, db_path): - self.db_path = db_path - - def add_region(self, region_id: int, region_name: str): - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - # Проверяем наличие региона - cursor.execute('SELECT region_name, active FROM regions WHERE region_id = ?', (region_id,)) - result = cursor.fetchone() - - if result: - existing_region_name, active = result - if existing_region_name == region_name: - # Регион уже существует с этим именем - cursor.execute('UPDATE regions SET active = TRUE WHERE region_id = ?', (region_id,)) - conn.commit() - return "activated", existing_region_name - else: - return "exists", existing_region_name - else: - # Добавляем новый регион - cursor.execute('INSERT OR IGNORE INTO regions (region_id, region_name) VALUES (?, ?)', (region_id, region_name)) - conn.commit() - return "added", region_name - - def remove_region(self, region_id: int): - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - # Проверяем, существует ли регион - cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,)) - count = cursor.fetchone()[0] - - if count == 0: - return False # Регион не найден - - # Деактивируем регион и обновляем подписки - cursor.execute('UPDATE regions SET active = FALSE WHERE region_id = ?', (region_id,)) - cursor.execute('UPDATE subscriptions SET active = FALSE WHERE region_id = ? AND active = TRUE', (region_id,)) - conn.commit() - return True - - def log_event(self, chat_id: int, username: str, action: str): - timestamp = time.strftime('%Y-%m-%d %H:%M:%S') - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)' - cursor.execute(query, (chat_id, username, action, timestamp)) - conn.commit() - -region_manager = RegionManager(DB_PATH) - -def process_add_region(message): - chat_id = message.chat.id - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - - try: - parts = message.text.split() - if len(parts) < 2: - raise ValueError("Неверный формат") - - region_id, region_name = parts[0], ' '.join(parts[1:]) - status, existing_region_name = region_manager.add_region(region_id, region_name) - - if status == "activated": - bot.send_message(chat_id, f"Регион {region_id} - {region_name} активирован.") - region_manager.log_event(chat_id, username, f"Admin reactivated region {region_id} - {region_name}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - elif status == "added": - bot.send_message(chat_id, f"Регион {region_id} - {region_name} добавлен.") - region_manager.log_event(chat_id, username, f"Admin added region {region_id} - {region_name}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - elif status == "exists": - markup = telebot.types.InlineKeyboardMarkup() - markup.add(telebot.types.InlineKeyboardButton(text="Заменить", - callback_data=f"replace_{region_id}_{urllib.parse.quote(region_name)}")) - markup.add( - telebot.types.InlineKeyboardButton(text="Активировать старый", callback_data=f"reactivate_{region_id}")) - markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_region_{chat_id}")) - bot.send_message(chat_id, - f"Регион {region_id} уже существует с названием {existing_region_name}. Хотите заменить его или активировать старый регион?", - reply_markup=markup) - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - except (IndexError, ValueError): - bot.send_message(chat_id, "Неверный формат. Используйте: ") - except Exception as e: - telebot.logger.error(f"Unexpected error: {e}") - bot.send_message(chat_id, "Произошла ошибка при обработке запроса.") - - -@bot.callback_query_handler(func=lambda call: call.data.startswith("replace_") or call.data.startswith( - "reactivate_") or call.data.startswith("cancel_region_")) -def handle_region_action(call): - parts = call.data.split("_", 2) - action = parts[0] - region_id = parts[1] - region_name = parts[2] if len(parts) > 2 else None - chat_id = call.message.chat.id - username = f"@{call.message.from_user.username}" if call.message.from_user.username else "N/A" - - if action == "replace": - if region_name: - region_name = urllib.parse.unquote(region_name) - region_manager.add_region(region_id, region_name) - bot.send_message(chat_id, f"Регион {region_id} обновлен до {region_name} и активирован.") - region_manager.log_event(chat_id, username, - f"Admin replaced and reactivated region {region_id} - {region_name}") - telebot.logger.info(f"Admin {username} replaced and reactivated region {region_id} - {region_name}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - - - elif action == "reactivate": - region_manager.add_region(region_id, region_name) - bot.send_message(chat_id, f"Регион {region_id} активирован.") - region_manager.log_event(chat_id, username, f"Admin reactivated region {region_id} - {region_name}") - telebot.logger.info(f"Admin {username} activate {region_id} - {region_name}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - - elif action == "cancel_region": - bot.send_message(chat_id, "Действие отменено.") - telebot.logger.info(f"Admin {username} canceled region actions.") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - bot.edit_message_reply_markup(chat_id=chat_id, message_id=call.message.message_id, reply_markup=None) - bot.answer_callback_query(call.id) - return show_settings_menu(chat_id) - -def process_remove_region(message): - chat_id = message.chat.id - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - - try: - region_id = message.text.split()[0] - success = region_manager.remove_region(region_id) - - if success: - bot.send_message(chat_id, f"Регион {region_id} теперь неактивен, и все активные подписки обновлены.") - region_manager.log_event(chat_id, username, f"Admin {username} deactivated region {region_id}") - telebot.logger.info(f"Admin {username} deactivated region {region_id}") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - else: - bot.send_message(chat_id, f"Регион с ID {region_id} не существует.") - user_state_manager.set_state(chat_id, "SETTINGS_MENU") - show_settings_menu(chat_id) - - except IndexError: - bot.send_message(chat_id, "Неверный формат. Используйте: ") - -###################################################################################################################### -## -## -###################################################################################################################### # Handle displaying active subscriptions for a user 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 is_whitelisted(chat_id): - bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + 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 = get_user_subscribed_regions(chat_id) + user_regions = bot_database.get_user_subscribed_regions(chat_id) if not user_regions: - bot.send_message(chat_id, "Вы не подписаны ни на один регион.") + 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 = format_regions_list(user_regions) - bot.send_message(chat_id, f"Ваши активные подписки:\n{regions_list}") + 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) @@ -795,199 +158,28 @@ def handle_my_subscriptions_button(message): 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 is_whitelisted(chat_id): - bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") + 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 = get_sorted_regions() # Используем функцию для получения отсортированных регионов + regions = bot_database.get_sorted_regions() # Используем функцию для получения отсортированных регионов if not regions: - bot.send_message(chat_id, "Нет активных регионов.") + backend_bot.bot.send_message(chat_id, "Нет активных регионов.") else: - regions_list = format_regions_list(regions) - bot.send_message(chat_id, f"Активные регионы:\n{regions_list}") + 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 rabbitmq_connection(): - # Создаем объект учетных данных - credentials = pika.PlainCredentials(RABBITMQ_LOGIN,RABBITMQ_PASS) - - # Указываем параметры подключения, включая учетные данные - parameters = pika.ConnectionParameters( - host=RABBITMQ_HOST, - credentials=credentials, # Передаем учетные данные - heartbeat=600, # Интервал heartbeat для поддержания соединения - 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, # make message persistent - )) - connection.close() - - -# async def consume_from_queue(): -# connection, channel = rabbitmq_connection() -# -# for method_frame, properties, body in channel.consume(RABBITMQ_QUEUE): -# message = json.loads(body) -# chat_id = message['chat_id'] -# username = message['username'] -# message_text = message['message'] -# -# try: -# await send_notification_message(chat_id, message_text, username) -# channel.basic_ack(method_frame.delivery_tag) -# except Exception as e: -# telebot.logger.error(f"Error sending message from queue: {e}") -# # Optionally, you can nack the message to requeue it -# # channel.basic_nack(method_frame.delivery_tag) -# -# connection.close() - - -async def consume_from_queue(): - while True: # Бесконечный цикл для переподключения - try: - # Подключение к RabbitMQ - 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) - - # Проверка структуры сообщения - if not isinstance(data, dict): - raise ValueError("Invalid message format: Expected a dictionary") - - # Извлечение необходимых данных - chat_id = data.get("chat_id") - username = data.get("username") - message_text = data.get("message") - - # Проверка обязательных полей - if not all([chat_id, username, message_text]): - raise ValueError(f"Missing required fields in message: {data}") - - # Отправляем сообщение - await send_notification_message(chat_id, message_text, username) - except json.JSONDecodeError: - # Логируем некорректный JSON - telebot.logger.error(f"Failed to decode message: {message.body}") - except ValueError as ve: - # Логируем ошибку формата сообщения - telebot.logger.error(f"Invalid message: {ve}") - except Exception as e: - # Логируем общую ошибку при обработке - telebot.logger.error(f"Error sending message from queue: {e}") - except aio_pika.exceptions.AMQPError as e: - # Логируем ошибку RabbitMQ и переподключаемся - telebot.logger.error(f"RabbitMQ error: {e}") - except Exception as e: - # Логируем общую ошибку и ждем перед переподключением - telebot.logger.error(f"Critical error in consume_from_queue: {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() - parse_mode = 'HTML' - - # Используем partial для передачи именованных аргументов в bot.send_message - func_with_args = partial(bot.send_message, chat_id=chat_id, text=message, parse_mode=parse_mode) - - # Передаем подготовленную функцию в run_in_executor - await run_in_executor(func_with_args) - - except telebot.apihelper.ApiTelegramException as e: - if "429" in str(e): - telebot.logger.warning(f"Rate limit exceeded for chat_id {chat_id}. Retrying...") - await asyncio.sleep(1) - await send_message(chat_id, message, is_notification) - elif "403" in str(e): - telebot.logger.warning(f"Can't send message to user because bot blocked by user with chat id: {chat_id}") - pass - else: - telebot.logger.error(f"Failed to send message to {chat_id}: {e}") - telebot.logger.error(f"Detailed Error: {e}", exc_info=True) # Добавлено логирование исключения - except Exception as e: - username = f"@{message.from_user.username}" if message.from_user.username else "N/A" - telebot.logger.error(f"Unexpected error while sending message to {username} {chat_id}: {e}", exc_info=True) - await check_telegram_api() - finally: - if is_notification: - rate_limit_semaphore.release() - formatted_message = message.replace('\n', ' ').replace('\r', '') - telebot.logger.info(f'Send notification to {chat_id} from RabbitMQ [{formatted_message}]') - - -async def send_notification_message(chat_id, message, username): - await send_message(chat_id, message, is_notification=True) - # formatted_message = message.replace('\n', ' ').replace('\r', '') - # telebot.logger.info(f'Send notification to {username} {chat_id} from RabbitMQ [{formatted_message}]') - - -async def run_in_executor(func, *args): - loop = asyncio.get_event_loop() - with ThreadPoolExecutor() as pool: - return await loop.run_in_executor(pool, func, *args) - - -async def check_telegram_api(): - try: - async with aiohttp.ClientSession() as session: - async with session.get('https://api.telegram.org') as response: - if response.status == 200: - telebot.logger.info("Telegram API is reachable.") - else: - telebot.logger.error("Telegram API is not reachable.") - except Exception as e: - telebot.logger.error(f"Error checking Telegram API: {e}") - - -def extract_region_number(host): - # Используем регулярное выражение для извлечения цифр после первого символа и до первой буквы - match = re.match(r'^.\d+', host) - if match: - return match.group(0)[1:] # Возвращаем строку без первого символа - return None - - 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 is_whitelisted(chat_id): - bot.send_message(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 @@ -999,11 +191,11 @@ def handle_notification_mode_button(message): markup.add(types.InlineKeyboardButton(text="Критические события", callback_data="notification_mode_disaster")) markup.add(types.InlineKeyboardButton(text="Все события", callback_data="notification_mode_all")) - bot.send_message(chat_id, - "Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n" - '1. Критические события (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n' - '2. Все события (По умолчанию) - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.', - reply_markup=markup, parse_mode="HTML") + backend_bot.bot.send_message(chat_id, + "Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n" + '1. Критические события (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n' + '2. Все события (По умолчанию) - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.', + reply_markup=markup, parse_mode="HTML") telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).") @@ -1017,7 +209,7 @@ def handle_notification_mode_selection(call): telebot.logger.debug(f"User ({chat_id}) selected notification mode: {mode}.") # Убираем клавиатуру - bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) + 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}).") # Обновляем режим уведомлений @@ -1033,7 +225,7 @@ def handle_notification_mode_selection(call): conn.commit() mode_text = "Критические события" if disaster_only else "Все события" - bot.send_message(chat_id, f"Режим уведомлений успешно изменён на: {mode_text}") + backend_bot.bot.send_message(chat_id, f"Режим уведомлений успешно изменён на: {mode_text}") telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}") # Логируем изменение состояния пользователя @@ -1045,7 +237,7 @@ def handle_notification_mode_selection(call): telebot.logger.debug(f"Displayed settings menu to {chat_id}.") except Exception as e: telebot.logger.error(f"Error updating notification mode for {chat_id}: {e}") - bot.send_message(chat_id, "Произошла ошибка при изменении режима уведомлений.") + backend_bot.bot.send_message(chat_id, "Произошла ошибка при изменении режима уведомлений.") finally: conn.close() telebot.logger.debug(f"Database connection closed for user {chat_id}.") @@ -1055,15 +247,14 @@ def handle_notification_mode_selection(call): telebot.logger.debug(f"Callback query for user ({chat_id}) answered.") - # Фаза 1: Запрос активных событий и выбор региона с постраничным переключением def handle_active_triggers(message): chat_id = message.chat.id - regions = get_sorted_regions() # Используем функцию get_regions для получения регионов + regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов start_index = 0 markup = create_region_keyboard(regions, start_index) - bot.send_message(chat_id, "Выберите регион для получения активных событий:", reply_markup=markup) + backend_bot.bot.send_message(chat_id, "Выберите регион для получения активных событий:", reply_markup=markup) def create_region_keyboard(regions, start_index, regions_per_page=10): @@ -1090,14 +281,14 @@ def create_region_keyboard(regions, start_index, regions_per_page=10): @bot.callback_query_handler( - func=lambda call: call.data.startswith("region_") or call.data.startswith("prev_") or call.data.startswith( - "next_")) + 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 = get_sorted_regions() # Используем функцию get_regions для получения регионов + regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов regions_per_page = 10 # Если был выбран регион, то убираем клавиатуру и продолжаем выполнение функции @@ -1132,11 +323,12 @@ def handle_region_selection(call, region_id): 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']] + 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: - bot.send_message(chat_id, "Нет групп хостов для этого региона.") + backend_bot.bot.send_message(chat_id, "Нет групп хостов для этого региона.") show_main_menu(chat_id) return @@ -1144,11 +336,13 @@ def handle_region_selection(call, region_id): 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}")) + markup.add(types.InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)", + callback_data=f"all_groups_{region_id}")) - bot.send_message(chat_id, "Выберите группу хостов или получите события по всем группам региона:", reply_markup=markup) + backend_bot.bot.send_message(chat_id, "Выберите группу хостов или получите события по всем группам региона:", + reply_markup=markup) except Exception as e: - bot.send_message(chat_id, f"Ошибка при подключении к Zabbix API.\n{str(e)}") + backend_bot.bot.send_message(chat_id, f"Ошибка при подключении к Zabbix API.\n{str(e)}") show_main_menu(chat_id) @@ -1174,649 +368,8 @@ def handle_group_or_all_groups(call): show_main_menu(chat_id) - -# Вспомогательная функция: получение событий для группы -def get_triggers_for_group(chat_id, group_id): - triggers = get_zabbix_triggers(group_id) # Получаем все активные события без периода - if not triggers: - bot.send_message(chat_id, f"Нет активных событий.") - show_main_menu(chat_id) - else: - send_triggers_to_user(triggers, chat_id) - - -def get_triggers_for_all_groups(chat_id, region_id): - 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()] - - all_triggers = [] - for group in filtered_groups: - triggers = get_zabbix_triggers(group['groupid']) - if triggers: - all_triggers.extend(triggers) - - if all_triggers: - send_triggers_to_user(all_triggers, chat_id) - else: - bot.send_message(chat_id, f"Нет активных событий.") - show_main_menu(chat_id) - except Exception as e: - bot.send_message(chat_id, f"Ошибка при получении событий.\n{str(e)}") - show_main_menu(chat_id) - - - -# Вспомогательная функция: отправка событий пользователю -def send_triggers_to_user(triggers, chat_id): - for trigger in triggers: - bot.send_message(chat_id, trigger, parse_mode="html") - time.sleep(1 / 5) - - -def escape_telegram_chars(text): - """ - Экранирует запрещённые символы для Telegram API: - < -> < - > -> > - & -> & - Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием. - """ - replacements = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', # Для кавычек - } - - # Применяем замены - for char, replacement in replacements.items(): - text = text.replace(char, replacement) - - return text - - -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): - try: - zapi = ZabbixAPI(ZABBIX_URL) - zapi.login(api_token=ZABBIX_API_TOKEN) - telebot.logger.info(f"Fetching active hosts for group {group_id}") - - # Получаем список активных хостов в группе - active_hosts = zapi.host.get( - groupids=group_id, - output=["hostid", "name"], - filter={"status": "0"} # Только включенные хосты - ) - - if not active_hosts: - telebot.logger.info(f"No active hosts found for group {group_id}") - return [] - - host_ids = [host["hostid"] for host in active_hosts] - telebot.logger.info(f"Found {len(host_ids)} active hosts in group {group_id}") - - # Получение активных проблем для этих хостов - problems = zapi.problem.get( - output=["eventid", "name", "severity", "clock", "objectid"], - hostids=host_ids, - suppressed=0, - acknowledged=0, - filter={"severity": ["4", "5"]}, # Только высокий и аварийный уровень - sortorder="ASC" - ) - - if not problems: - telebot.logger.info(f"No active problems found for group {group_id}") - return [] - - # Получение триггеров и их связи с хостами - trigger_ids = [problem["objectid"] for problem in problems] - triggers = zapi.trigger.get( - triggerids=trigger_ids, - output=["triggerid", "description"], - selectHosts=["hostid"] - ) - - # Создаем карту триггеров к хостам - trigger_to_host_map = {} - for trigger in triggers: - if "hosts" in trigger and trigger["hosts"]: - trigger_to_host_map[trigger["triggerid"]] = trigger["hosts"][0]["hostid"] - - # Получение IP-адресов хостов - host_interfaces = zapi.hostinterface.get( - hostids=host_ids, - output=["hostid", "ip"] - ) - host_ip_map = {iface["hostid"]: iface["ip"] for iface in host_interfaces} - # print(host_ip_map) - - # Получение itemid для триггеров на основе их описания - item_map = {} - for host in active_hosts: - host_id = host["hostid"] - items = zapi.item.get( - hostids=host_id, - output=["itemid", "name"] - ) - for item in items: - item_map[(host_id, item["name"])] = item["itemid"] - - - moscow_tz = timezone('Europe/Moscow') - severity_map = {'4': 'HIGH', '5': 'DISASTER'} - priority_map = {'4': '⚠️', '5': '⛔️'} - problem_messages = [] - - for problem in problems: - event_time_epoch = int(problem['clock']) - event_time = datetime.fromtimestamp(event_time_epoch, tz=moscow_tz) - event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') - - severity = severity_map.get(problem['severity'], 'Неизвестно') - priority = priority_map.get(problem['severity'], '') - description = problem.get('name', 'Нет описания') - - # Получаем hostid через триггер - trigger_id = problem.get("objectid") - host_id = trigger_to_host_map.get(trigger_id, "Неизвестный хост") - host_ip = host_ip_map.get(host_id, "Неизвестный IP") - host = extract_host_from_name(description) - description = escape_telegram_chars(description) - - - - message = (f"{priority} {host} ({host_ip})\n" - f"Описание: {description}\n" - f"Критичность: {severity}\n" - f"Время создания: {event_time_formatted}") - - problem_messages.append(message) - - return problem_messages - except Exception as e: - telebot.logger.error(f"Error fetching problems for group {group_id}: {e}") - return None - - -@app.route(BASE_URL + '/webhook', methods=['POST']) -def webhook(): - try: - # Получаем данные и логируем - data = request.get_json() - app.logger.info(f"Получены данные: {data}") - - # Генерация хеша события и логирование - event_hash = hash_data(data) - app.logger.debug(f"Сгенерирован хеш для события: {event_hash}") - - # Работа с базой данных в блоке синхронизации - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # Проверяем количество записей в таблице событий - cursor.execute('SELECT COUNT(*) FROM events') - count = cursor.fetchone()[0] - app.logger.debug(f"Текущее количество записей в таблице events: {count}") - - # Если записей >= 200, удаляем самое старое событие - if count >= 200: - query = 'DELETE FROM events WHERE id = (SELECT MIN(id) FROM events)' - app.logger.debug(f"Удаление старого события: {query}") - cursor.execute(query) - - # Извлечение номера региона из поля host - region_id = extract_region_number(data.get("host")) - if region_id is None: - app.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}") - return jsonify({"status": "error", "message": "Invalid host format"}), 400 - app.logger.debug(f"Извлечён номер региона: {region_id}") - - # Запрос подписчиков для отправки уведомления в зависимости от уровня критичности - if data['severity'] == 'Disaster': # Авария - query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE' - else: # Высокая критичность - query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE AND disaster_only = FALSE' - - app.logger.debug(f"Выполнение запроса: {query} для region_id={region_id}") - cursor.execute(query, (region_id,)) - results = cursor.fetchall() - - app.logger.debug(f"Найдено подписчиков: {len(results)} для региона {region_id}") - - # Проверка статуса региона (активен или нет) - query = 'SELECT active FROM regions WHERE region_id = ?' - cursor.execute(query, (region_id,)) - region_row = cursor.fetchone() - - if region_row and region_row[0]: # Если регион активен - app.logger.debug(f"Регион {region_id} активен. Начинаем рассылку сообщений.") - message = format_message(data) - undelivered = False - - # Отправляем сообщения подписчикам - for chat_id, username in results: - formatted_message = message.replace('\n',' ').replace('\r','') - - app.logger.info(f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]") - try: - send_to_queue({'chat_id': chat_id, 'username': username, 'message': message}) - app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})") - except Exception as e: - app.logger.error(f"Ошибка при отправке сообщения для {chat_id} (@{username}): {e}") - undelivered = True - - # Сохранение события, если были проблемы с доставкой - if undelivered: - query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)' - app.logger.debug(f"Сохранение события в базе данных: {query} (hash={event_hash}, delivered={False})") - cursor.execute(query, (event_hash, str(data), False)) - - # Коммитим изменения в базе данных - conn.commit() - app.logger.debug("Изменения в базе данных успешно сохранены.") - conn.close() - - # Возвращаем успешный ответ - return jsonify({"status": "success"}), 200 - - except sqlite3.OperationalError as e: - app.logger.error(f"Ошибка операции с базой данных: {e}") - return jsonify({"status": "error", "message": "Ошибка работы с базой данных"}), 500 - - except ValueError as e: - app.logger.error(f"Ошибка значения: {e}") - return jsonify({"status": "error", "message": "Некорректные данные"}), 400 - - except Exception as e: - app.logger.error(f"Неожиданная ошибка: {e}") - return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500 - - - -def format_message(data): - try: - priority_map = { - 'High': '⚠️', - 'Disaster': '⛔️' - } - priority = priority_map.get(data['severity']) - msg = escape_telegram_chars(data['msg']) - if data['status'].upper() == "PROBLEM": - message = ( - f"{priority} {data['host']} ({data['ip']})\n" - f"Описание: {msg}\n" - f"Критичность: {data['severity']}\n" - f"Время возникновения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" - ) - if 'link' in data: - message += f'URL: Ссылка на график' - return message - else: - message = ( - f"✅ {data['host']} ({data['ip']})\n" - f"Описание: {msg}\n" - f"Критичность: {data['severity']}\n" - f"Проблема устранена!\n" - f"Время устранения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" - ) - if 'link' in data: - message += f'URL: Ссылка на график' - return message - except KeyError as e: - app.logger.error(f"Missing key in data: {e}") - raise ValueError(f"Missing key in data: {e}") - - -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 - - -# Маршрут для добавления пользователя -@app.route(BASE_URL + '/users/add', methods=['POST']) -def add_user(): - data = request.get_json() - - telegram_id = data.get('telegram_id') - chat_id = data.get('chat_id') - user_email = data.get('user_email') - - # DEBUG: Логирование полученных данных - app.logger.debug(f"Получены данные для добавления пользователя: {data}") - - # Валидация данных - if not validate_chat_id(chat_id): - app.logger.warning(f"Ошибка валидации: некорректный chat_id: {chat_id}") - return jsonify({"status": "failure", "reason": "Invalid data chat_id must be digit"}), 400 - - if not validate_telegram_id(telegram_id): - app.logger.warning(f"Ошибка валидации: некорректный telegram_id: {telegram_id}") - return jsonify({"status": "failure", "reason": "Invalid data telegram id must start from '@'"}), 400 - - if not validate_email(user_email): - app.logger.warning(f"Ошибка валидации: некорректный email: {user_email}") - return jsonify({"status": "failure", "reason": "Invalid data email address must be from rtmis"}), 400 - - if telegram_id and chat_id and user_email: - try: - # INFO: Попытка отправить сообщение пользователю - app.logger.info(f"Отправка сообщения пользователю {telegram_id} с chat_id {chat_id}") - bot.send_message(chat_id, "Регистрация пройдена успешно.") - # DEBUG: Попытка добавления пользователя в whitelist - app.logger.debug(f"Добавление пользователя {telegram_id} в whitelist") - success = rundeck_add_to_whitelist(chat_id, telegram_id, user_email) - if success: - # INFO: Пользователь успешно добавлен в whitelist - app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.") - user_state_manager.set_state(chat_id, "MAIN_MENU") - - # DEBUG: Показ основного меню пользователю - app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}") - show_main_menu(chat_id) - return jsonify({"status": "success", "msg": f"User {telegram_id} with {user_email} added successfully"}), 200 - else: - # INFO: Пользователь уже существует в системе - app.logger.info(f"Пользователь с chat_id {chat_id} уже существует.") - return jsonify({"status": "failure", "msg": "User already exists"}), 400 - except telebot.apihelper.ApiTelegramException as e: - if e.result.status_code == 403: - # INFO: Пользователь заблокировал бота - app.logger.info(f"Пользователь {telegram_id} заблокировал бота") - return jsonify({"status": "failure", "msg": f"User {telegram_id} is blocked chat with bot"}) - elif e.result.status_code == 400: - # WARNING: Пользователь неизвестен боту, возможно не нажал /start - app.logger.warning(f"Пользователь {telegram_id} с chat_id {chat_id} неизвестен боту, возможно, не нажал /start") - return jsonify({"status": "failure", "msg": f"User {telegram_id} with {chat_id} is unknown to the bot, did the user press /start button?"}) - else: - # ERROR: Неизвестная ошибка при отправке сообщения - app.logger.error(f"Ошибка при отправке сообщения пользователю {telegram_id}: {str(e)}") - return jsonify({"status": "failure", "msg": f"{e}"}) - else: - # ERROR: Ошибка валидации — недостаточно данных - app.logger.error("Получены некорректные данные для добавления пользователя.") - return jsonify({"status": "failure", "reason": "Invalid data"}), 400 - - - -@app.route(BASE_URL + '/users/del', methods=['POST']) -def delete_user(): - data = request.get_json() - user_email = data.get('email') - conn = sqlite3.connect(DB_PATH) - try: - # DEBUG: Получен запрос и начинается обработка - app.logger.debug(f"Получен запрос на удаление пользователя. Данные: {data}") - - if not user_email: - # WARNING: Ошибка валидации данных, email отсутствует - app.logger.warning(f"Ошибка валидации: отсутствует email") - return jsonify({"status": "failure", "message": "Email is required"}), 400 - - cursor = conn.cursor() - - # DEBUG: Запрос на получение chat_id - app.logger.debug(f"Выполняется запрос на получение chat_id для email: {user_email}") - cursor.execute("SELECT chat_id FROM whitelist WHERE user_email = ?", (user_email,)) - user = cursor.fetchone() - - if user is None: - # WARNING: Пользователь с указанным email не найден - app.logger.warning(f"Пользователь с email {user_email} не найден") - return jsonify({"status": "failure", "message": "User not found"}), 404 - chat_id = user[0] - - # INFO: Удаление пользователя и его подписок начато - app.logger.info(f"Начато удаление пользователя с email {user_email} и всех его подписок") - - # DEBUG: Удаление пользователя из whitelist - app.logger.debug(f"Удаление пользователя с email {user_email} из whitelist") - cursor.execute("DELETE FROM whitelist WHERE user_email = ?", (user_email,)) - - # DEBUG: Удаление подписок пользователя - app.logger.debug(f"Удаление подписок для пользователя с chat_id {chat_id}") - cursor.execute("DELETE FROM subscriptions WHERE chat_id = ?", (chat_id,)) - - conn.commit() - # INFO: Пользователь и подписки успешно удалены - app.logger.info(f"Пользователь с email {user_email} и все его подписки успешно удалены") - return jsonify( - {"status": "success", "message": f"User with email {user_email} and all subscriptions deleted."}), 200 - except Exception as e: - conn.rollback() - # ERROR: Ошибка при удалении данных - app.logger.error(f"Ошибка при удалении пользователя с email {user_email}: {str(e)}") - return jsonify({"status": "failure", "message": str(e)}), 500 - finally: - conn.close() - # DEBUG: Соединение с базой данных закрыто - app.logger.debug(f"Соединение с базой данных закрыто") - - -# Маршрут для получения информации о пользователях -@app.route(BASE_URL + '/users/get', methods=['GET']) -def get_users(): - try: - # INFO: Запрос на получение списка пользователей - app.logger.info("Запрос на получение информации о пользователях получен") - - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # DEBUG: Запрос данных из таблицы whitelist - app.logger.debug("Запрос данных пользователей из таблицы whitelist") - cursor.execute('SELECT * FROM whitelist') - users = cursor.fetchall() - - # DEBUG: Формирование словаря пользователей - app.logger.debug("Формирование словаря пользователей") - users_dict = {id: {'id': id, 'username': username, 'email': email, 'events': [], 'worker': '', 'subscriptions': []} - for id, username, email in users} - - # DEBUG: Запрос данных событий пользователей - app.logger.debug("Запрос событий пользователей из таблицы user_events") - cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') - events = cursor.fetchall() - - # DEBUG: Обработка событий и добавление их в словарь пользователей - for chat_id, username, action, timestamp in events: - if chat_id in users_dict: - event = {'type': action, 'date': timestamp} - if "Subscribed to region" in action: - region = action.split(": ")[-1] - event['region'] = region - users_dict[chat_id]['events'].append(event) - - # DEBUG: Запрос данных подписок пользователей - app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions") - cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') - subscriptions = cursor.fetchall() - - # DEBUG: Добавление подписок к пользователям - for chat_id, region_id in subscriptions: - if chat_id in users_dict: - users_dict[chat_id]['subscriptions'].append(str(region_id)) - - # INFO: Формирование результата - app.logger.info("Формирование результата для ответа") - result = [] - for user in users_dict.values(): - ordered_user = { - 'email': user['email'], - 'username': user['username'], - 'id': user['id'], - 'worker': user['worker'], - 'events': user['events'], - 'subscriptions': ', '.join(user['subscriptions']) - } - result.append(ordered_user) - - # INFO: Успешная отправка данных пользователей - app.logger.info("Информация о пользователях успешно отправлена") - return jsonify(result) - - except Exception as e: - # ERROR: Ошибка при получении информации о пользователях - app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}") - return jsonify({'status': 'error', 'message': str(e)}), 500 - - - -# Маршрут для отображения HTML-страницы с информацией о пользователях -@app.route(BASE_URL + '/users', methods=['GET']) -def view_users(): - return render_template('users.html') - - -# Маршрут для добавления региона -@app.route(BASE_URL + '/regions/add', methods=['POST']) -def add_region(): - data = request.json - region_id: int = data.get('region_id') - region_name: str = data.get('region_name') - - if not region_id or not region_name: - return jsonify({"status": "error", "message": "Invalid input"}), 400 - - result = region_api.add_region(region_id, region_name) - return jsonify(result) - - -# Маршрут для удаления региона -@app.route(BASE_URL + '/regions/del', methods=['POST']) -def del_region(): - data = request.json - region_id = data.get('region_id') - - if not region_id: - return jsonify({"status": "error", "message": "Invalid input"}), 400 - - result = region_api.remove_region(region_id) - return jsonify(result) - - -# Маршрут для получения списка регионов -@app.route(BASE_URL + '/regions/get', methods=['GET']) -def get_regions(): - regions = region_api.get_regions() - return jsonify(regions) - -@app.route(BASE_URL + '/regions/edit', methods=['POST']) -def edit_region(): - data = request.json - region_id = data.get('region_id') - active = data.get('active') - - # Проверка валидности данных - if not region_id and active: - return jsonify({"status": "error", "message": "Invalid data received"}), 400 - - # Обновление региона - result = region_api.update_region_status(region_id, active) - return jsonify(result) - - -# Маршрут для рендеринга страницы управления регионами -@app.route(BASE_URL + '/regions', methods=['GET']) -def regions_page(): - - return render_template('regions.html') - - -# Управление уровнями логирования для Flask -@app.route(BASE_URL + '/debug/flask', methods=['POST']) -def toggle_flask_debug(): - try: - data = request.get_json() - level = data.get('level').upper() - if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: - return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400 - - log_level = getattr(logging, level, logging.DEBUG) - app.logger.setLevel(log_level) - - for handler in app.logger.handlers: - handler.setLevel(log_level) - - return jsonify({'status': 'success', 'level': level}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - - -# Управление уровнями логирования для Telebot -@app.route(BASE_URL + '/debug/telebot', methods=['POST']) -def toggle_telebot_debug(): - try: - data = request.get_json() - level = data.get('level').upper() - if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: - return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400 - - log_level = getattr(logging, level, logging.DEBUG) - telebot.logger.setLevel(log_level) - - for handler in telebot.logger.handlers: - handler.setLevel(log_level) - - return jsonify({'status': 'success', 'level': level}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - -# Test functions for admin -def simulate_event(message): - chat_id = message.chat.id - test_event = { - "host": "12", - "msg": "Тестовое сообщение", - "date_reception": "12757175125", - "severity": "5", - "tags": "OTC,OFS,NFS", - "status": "Авария!" - } - - app.logger.info(f"Simulating event: {test_event}") - # Use requests to simulate a POST request - response = requests.post('http://localhost:5000/webhook', json=test_event) - app.logger.info(f"Response from webhook: {response.status_code} - {response.text}") - bot.send_message(chat_id, f"Тестовое событие отправлено. Статус ответа: {response.status_code}") - - -def simulate_triggers(message): - chat_id = message.chat.id - regions = ["12", "19", "35", "40"] - trigger_messages = [] - for region_id in regions: - triggers = get_zabbix_triggers(region_id) - if triggers: - trigger_messages.append(f"Регион {region_id}:\n{triggers}") - - if trigger_messages: - bot.send_message(chat_id, "\n\n".join(trigger_messages), parse_mode="html") - else: - bot.send_message(chat_id, "Нет активных проблем по указанным регионам.") - - def run_polling(): - bot.infinity_polling(timeout=10, long_polling_timeout = 5) + bot.infinity_polling(timeout=10, long_polling_timeout=5) # Запуск Flask-приложения @@ -1824,11 +377,10 @@ def run_flask(): app.run(port=5000, host='0.0.0.0', debug=True, use_reloader=False) - # Основная функция для запуска def main(): # Инициализация базы данных - init_db() + bot_database.init_db() # Запуск Flask и бота в отдельных потоках Thread(target=run_flask, daemon=True).start() @@ -1839,4 +391,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e8b72f2 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,10 @@ + + + + + Index of WebUI for Telezab + + +THIS IS WEBUI FOR TELEZAB BOT + + \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..33d19ba --- /dev/null +++ b/utils.py @@ -0,0 +1,117 @@ +import re +import time + +import telebot + +import backend_bot +import backend_flask +import bot_database +import telezab + + +def validate_chat_id(chat_id): + """Validate that chat_id is composed only of digits.""" + return chat_id.isdigit() + + +def validate_telegram_id(telegram_id): + """Validate that telegram_id starts with '@'.""" + return telegram_id.startswith('@') + + +def validate_email(email): + """Validate that email domain is '@rtmis.ru'.""" + return re.match(r'^[\w.-]+@rtmis\.ru$', email) is not None + + +def format_message(data): + try: + priority_map = { + 'High': '⚠️', + 'Disaster': '⛔️' + } + priority = priority_map.get(data['severity']) + msg = escape_telegram_chars(data['msg']) + if data['status'].upper() == "PROBLEM": + message = ( + f"{priority} {data['host']} ({data['ip']})\n" + f"Описание: {msg}\n" + f"Критичность: {data['severity']}\n" + f"Время возникновения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" + ) + if 'link' in data: + message += f'URL: Ссылка на график' + return message + else: + message = ( + f"✅ {data['host']} ({data['ip']})\n" + f"Описание: {msg}\n" + f"Критичность: {data['severity']}\n" + f"Проблема устранена!\n" + f"Время устранения: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))} Мск\n" + ) + if 'link' in data: + message += f'URL: Ссылка на график' + return message + except KeyError as e: + backend_flask.app.logger.error(f"Missing key in data: {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.user_state_manager.set_state(chat_id, "MAIN_MENU") + markup.add('Настройки', 'Помощь', 'Активные события') + else: + telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + markup.add('Регистрация') + backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) + + +def create_settings_keyboard(chat_id, admins_list): + 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.user_state_manager.set_state(chat_id, "REGISTRATION") + backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") + return + admins_list = bot_database.get_admins() + markup = create_settings_keyboard(chat_id, admins_list) + backend_bot.bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup) diff --git a/webui/__init__.py b/webui/__init__.py new file mode 100644 index 0000000..693aee4 --- /dev/null +++ b/webui/__init__.py @@ -0,0 +1,19 @@ +from email.policy import default +from jinja2 import TemplateNotFound + +from flask import Blueprint, render_template, jsonify, abort + +webui = Blueprint('webui', __name__, url_prefix='/telezab') + + +@webui.route('/', defaults={'index'}) +def index(): + try: + return render_template(index.html) + except TemplateNotFound: + abort(404) + + +@webui.route('/data') +def get_data(): + return jsonify({"message": "Данные из frontend!"}) diff --git a/webui/index.py b/webui/index.py new file mode 100644 index 0000000..20e12e2 --- /dev/null +++ b/webui/index.py @@ -0,0 +1,13 @@ +from flask import Blueprint, render_template, jsonify + +webui = Blueprint('webui', __name__, url_prefix='/telezab') + +@webui.route("/heartbeat") +def heartbeat(): + return jsonify({"status": "healthy"}) + + +@webui.route('/', defaults={'path': ''}) +@webui.route('/') +def catch_all(path): + return webui.send_static_file("index.html") \ No newline at end of file diff --git a/zabbix_manager.py b/zabbix_manager.py deleted file mode 100644 index c2d0974..0000000 --- a/zabbix_manager.py +++ /dev/null @@ -1,116 +0,0 @@ -import logging -import asyncio -from pyzabbix import ZabbixAPI -from datetime import datetime -from pytz import timezone -import os - - - -class ZabbixTriggerManager: - def __init__(self): - self.zabbix_url = os.getenv('ZABBIX_URL') # URL Zabbix из переменных окружения - self.api_token = os.getenv('ZABBIX_API_TOKEN') # API токен Zabbix из переменных окружения - self.moskva_tz = timezone('Europe/Moscow') # Часовой пояс - self.priority_map = {'4': 'HIGH', '5': 'DISASTER'} # Отображение приоритетов - self.zapi = None # Клиент Zabbix API - - def login(self) -> None: - """ - Авторизация в Zabbix API. - """ - try: - self.zapi = ZabbixAPI(self.zabbix_url) - self.zapi.login(api_token=self.api_token) - logging.info("Successfully logged in to Zabbix API.") - except Exception as e: - logging.error(f"Error logging in to Zabbix API: {e}") - raise - - def get_problems_for_group(self, group_id: str) -> list: - """ - Получает проблемы для указанной группы хостов. - """ - try: - logging.info(f"Fetching problems for group {group_id}") - problems = self.zapi.problem.get( - output=["eventid", "name", "severity", "clock"], - groupids=group_id, - recent=1, # Только текущие проблемы - # sortfield=["clock"], - # sortorder="DESC" - ) - logging.info(f"Found {len(problems)} problems for group {group_id}") - return problems - except Exception as e: - logging.error(f"Error fetching problems for group {group_id}: {e}") - return [] - - def get_problems_for_region(self, region_id: str) -> list: - """ - Получает проблемы для всех групп, связанных с регионом. - """ - try: - logging.info(f"Fetching host groups for region {region_id}") - host_groups = self.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'] - ] - - all_problems = [] - for group in filtered_groups: - problems = self.get_problems_for_group(group['groupid']) - all_problems.extend(problems) - - return all_problems - except Exception as e: - logging.error(f"Error fetching problems for region {region_id}: {e}") - return [] - - async def send_problems_to_user(self, chat_id: int, problems: list, bot) -> None: - """ - Отправляет список проблем пользователю в Telegram с учётом задержки для обхода лимита API. - """ - if not problems: - logging.info(f"No problems to send to chat_id {chat_id}.") - bot.send_message(chat_id, "Нет активных проблем.") - return - - for problem in problems: - try: - # Форматируем время - event_time = datetime.fromtimestamp(int(problem['clock']), tz=self.moskva_tz) - event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск') - - # Определяем критичность - severity = self.priority_map.get(problem['severity'], "Неизвестно") - priority_mark = '⚠️' if severity == 'HIGH' else '⛔️' if severity == 'DISASTER' else '' - - # Определяем имя хоста - hosts = self._get_problem_hosts(problem) - host = hosts[0]["name"] if hosts else "Неизвестно" - - # Генерируем URL для графика - item_ids = [item["itemid"] for item in problem.get("items", [])] - url = f"{self.zabbix_url}/history.php?action=batchgraph&" + \ - "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) + "&graphtype=0" - - # Формируем сообщение - description = self.escape_telegram_chars(problem['name']) - message = ( - f"{priority_mark} Host: {host}\n" - f"Описание: {description}\n" - f"Критичность: {severity}\n" - f"Время создания: {event_time_formatted}\n" - f'URL: Ссылка на график' - ) - - # Отправляем сообщение - bot.send_message(chat_id, message, parse_mode="HTML") - - # Задержка для соблюдения рейтлимита - await asyncio.sleep(0.04) # 40 мс = максимум 25 сообщений в секунду - except Exception as e: - logging.error(f"Error sending problem to chat_id {chat_id}: {e}") -