Refactoring and cleanup codebase (starting...)

This commit is contained in:
Udo Chudo 2025-02-23 14:27:48 +05:00
parent d0e91f5259
commit 8a8cf8af30
16 changed files with 1319 additions and 1741 deletions

177
backend_bot.py Normal file
View File

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

348
backend_flask.py Normal file
View File

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

9
backend_locks.py Normal file
View File

@ -0,0 +1,9 @@
import threading
import telebot
db_lock = threading.Lock()
# bot_instance.py
from config import TOKEN
bot = telebot.TeleBot(TOKEN)

128
backend_zabbix.py Normal file
View File

@ -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"<b>{priority} Host</b>: {host}\n"
f"<b>IP</b>: {host_ip}\n"
f"<b>Описание</b>: {description}\n"
f"<b>Критичность</b>: {severity}\n"
f"<b>Время создания</b>: {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

238
bot_database.py Normal file
View File

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

15
config.py Normal file
View File

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

View File

@ -1,9 +1,10 @@
import logging import logging
from logging.config import dictConfig
from logging.handlers import TimedRotatingFileHandler
import os import os
import zipfile import zipfile
from datetime import datetime, timedelta from datetime import datetime, timedelta
from logging.config import dictConfig
from logging.handlers import TimedRotatingFileHandler
class UTF8StreamHandler(logging.StreamHandler): class UTF8StreamHandler(logging.StreamHandler):
def __init__(self, stream=None): def __init__(self, stream=None):
@ -15,11 +16,13 @@ class UTF8StreamHandler(logging.StreamHandler):
if hasattr(stream, 'reconfigure'): if hasattr(stream, 'reconfigure'):
stream.reconfigure(encoding='utf-8') stream.reconfigure(encoding='utf-8')
class FilterByMessage(logging.Filter): class FilterByMessage(logging.Filter):
def filter(self, record): def filter(self, record):
# Фильтруем сообщения, содержащие 'Received 1 new updates' # Фильтруем сообщения, содержащие 'Received 1 new updates'
return 'Received ' not in record.getMessage() return 'Received ' not in record.getMessage()
class LogManager: class LogManager:
def __init__(self, log_dir='logs', retention_days=30): def __init__(self, log_dir='logs', retention_days=30):
self.log_dir = log_dir self.log_dir = log_dir
@ -166,7 +169,8 @@ class LogManager:
werkzeug_logger.handlers = [] # Удаляем существующие обработчики 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')) handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s %(message)s'))
werkzeug_logger.addHandler(handler) werkzeug_logger.addHandler(handler)
@ -198,4 +202,3 @@ class LogManager:
"""Rotates and archives logs.""" """Rotates and archives logs."""
self.archive_old_logs() self.archive_old_logs()
self.schedule_log_rotation() # Schedule the next rotation self.schedule_log_rotation() # Schedule the next rotation

155
rabbitmq.py Normal file
View File

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

View File

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

Binary file not shown.

1614
telezab.py

File diff suppressed because it is too large Load Diff

10
templates/index.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index of WebUI for Telezab</title>
</head>
<body>
THIS IS WEBUI FOR TELEZAB BOT
</body>
</html>

117
utils.py Normal file
View File

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

19
webui/__init__.py Normal file
View File

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

13
webui/index.py Normal file
View File

@ -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('/<path:path>')
def catch_all(path):
return webui.send_static_file("index.html")

View File

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