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 aeb851b..dcf90c6 100644
Binary files a/requirements.txt and b/requirements.txt differ
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}")
-