Compare commits
2 Commits
d0e91f5259
...
20465600b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 20465600b1 | |||
| 8a8cf8af30 |
177
backend_bot.py
Normal file
177
backend_bot.py
Normal 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
348
backend_flask.py
Normal 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
9
backend_locks.py
Normal 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
128
backend_zabbix.py
Normal 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
238
bot_database.py
Normal 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
15
config.py
Normal 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}/"
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
156
rabbitmq.py
Normal file
156
rabbitmq.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
# Потребляем сообщения
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
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}")
|
||||||
@ -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()
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
1590
telezab.py
1590
telezab.py
File diff suppressed because it is too large
Load Diff
10
templates/index.html
Normal file
10
templates/index.html
Normal 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
117
utils.py
Normal 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:
|
||||||
|
< -> <
|
||||||
|
> -> >
|
||||||
|
& -> &
|
||||||
|
Также проверяет на наличие запрещённых 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)
|
||||||
18
webui/__init__.py
Normal file
18
webui/__init__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
13
webui/index.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from flask import Blueprint, 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():
|
||||||
|
return webui.send_static_file("index.html")
|
||||||
@ -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}")
|
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user