Telezab/telezab.py

395 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging
import sqlite3
from threading import Thread
import telebot
from pyzabbix import ZabbixAPI
from telebot import types
import backend_bot
import bot_database
from backend_flask import app
from backend_locks import bot
from backend_locks import db_lock
from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups
from config import *
from log_manager import LogManager
from rabbitmq import consume_from_queue
from user_state_manager import UserStateManager
from utils import show_main_menu, show_settings_menu
# Инициализируем класс UserStateManager
user_state_manager = UserStateManager()
# Инициализация LogManager
log_manager = LogManager(log_dir='logs', retention_days=30)
# Настройка pyTelegramBotAPI logger
telebot.logger = logging.getLogger('telebot')
# Важно: вызов schedule_log_rotation для планировки ротации и архивации логов
log_manager.schedule_log_rotation()
# # Lock for database operations
# db_lock = Lock()
# 25 messages per second
# Handle /help command to provide instructions
@bot.message_handler(commands=['help'])
def handle_help(message):
chat_id = message.chat.id
if not bot_database.is_whitelisted(chat_id):
backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.")
return
help_text = (
'<b>/start</b> - Показать меню бота\n'
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
'<b>Активные события</b> - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
'<b>Помощь</b> - <a href="https://confluence.is-mis.ru/pages/viewpage.action?pageId=416785183">Описание всех возможностей бота</a>'
)
backend_bot.bot.send_message(message.chat.id, help_text, parse_mode="html")
show_main_menu(message.chat.id)
# Handle /register command for new user registration
def handle_register(message):
chat_id = message.chat.id
username = message.from_user.username
if username:
username = f"@{username}"
else:
username = "N/A"
text = (
f'Для продолжения регистрации необходимо отправить с корпоративного почтового адреса "РТ МИС" письмо на адрес {SUPPORT_EMAIL}\n'
f'В теме письма указать "<b>Подтверждение регистрации в телеграм-боте TeleZab</b>".\n'
f'В теле письма указать:\n'
f'1. <b>ФИО</b>\n'
f'2. <b>Ваш Chat ID</b>: {chat_id}\n'
f'3. <b>Ваше имя пользователя</b>: {username}')
backend_bot.bot.send_message(chat_id, text, parse_mode="HTML")
bot_database.log_user_event(chat_id, username, "Requested registration")
# Handle /start command
@bot.message_handler(commands=['start'])
def handle_start(message):
show_main_menu(message.chat.id)
# Settings menu for users
# Основной обработчик меню
@bot.message_handler(func=lambda message: True)
def handle_menu_selection(message):
chat_id = message.chat.id
text = message.text.strip()
username = message.from_user.username
# Проверка авторизации
if not bot_database.is_whitelisted(chat_id) and text != 'Регистрация':
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
return
# Получаем текущее состояние пользователя
current_state = user_state_manager.get_state(chat_id)
# Обработка команд в зависимости от состояния
if current_state == "MAIN_MENU":
backend_bot.handle_main_menu(message, chat_id, text)
elif current_state == "REGISTRATION":
handle_register(message)
elif current_state == "SETTINGS_MENU":
backend_bot.handle_settings_menu(message, chat_id, text)
elif current_state == "SUBSCRIBE":
backend_bot.process_subscription_button(message, chat_id, username)
elif current_state == "UNSUBSCRIBE":
backend_bot.process_unsubscription_button(message, chat_id, username)
else:
backend_bot.bot.send_message(chat_id, "Команда не распознана.")
show_main_menu(chat_id)
@bot.callback_query_handler(func=lambda call: call.data == "cancel_action")
def handle_cancel_action(call):
chat_id = call.message.chat.id
message_id = call.message.message_id
backend_bot.bot.clear_step_handler_by_chat_id(chat_id)
backend_bot.bot.send_message(chat_id, f"Действие отменено")
backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None)
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
return
@bot.callback_query_handler(func=lambda call: call.data == "cancel_active_triggers")
def handle_cancel_active_triggers(call):
chat_id = call.message.chat.id
message_id = call.message.message_id
backend_bot.bot.clear_step_handler_by_chat_id(chat_id)
backend_bot.bot.send_message(chat_id, f"Действие отменено")
backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None)
user_state_manager.set_state(chat_id, "MAIN_MENU")
show_main_menu(chat_id)
return
# Handle displaying active subscriptions for a user
def handle_my_subscriptions_button(message):
chat_id = message.chat.id
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
if not bot_database.is_whitelisted(chat_id):
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
return
user_regions = bot_database.get_user_subscribed_regions(chat_id)
if not user_regions:
backend_bot.bot.send_message(chat_id, "Вы не подписаны ни на один регион.")
telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}")
else:
user_regions.sort(key=lambda x: int(x[0])) # Сортировка по числовому значению region_id
regions_list = bot_database.format_regions_list(user_regions)
backend_bot.bot.send_message(chat_id, f"Ваши активные подписки:\n{regions_list}")
telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}")
show_settings_menu(chat_id)
# Handle displaying all active regions
def handle_active_regions_button(message):
chat_id = message.chat.id
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
if not bot_database.is_whitelisted(chat_id):
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
return
regions = bot_database.get_sorted_regions() # Используем функцию для получения отсортированных регионов
if not regions:
backend_bot.bot.send_message(chat_id, "Нет активных регионов.")
else:
regions_list = bot_database.format_regions_list(regions)
backend_bot.bot.send_message(chat_id, f"Активные регионы:\n{regions_list}")
show_settings_menu(chat_id)
def handle_notification_mode_button(message):
chat_id = message.chat.id
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
telebot.logger.debug(f"Handling notification mode button for user {username} ({chat_id}).")
if not bot_database.is_whitelisted(chat_id):
backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота")
telebot.logger.warning(f"Unauthorized access attempt by {username} ({chat_id})")
return
# Логируем успешное авторизованное использование бота
telebot.logger.info(f"User {username} ({chat_id}) is authorized and is selecting a notification mode.")
# Отправляем клавиатуру выбора режима уведомлений
markup = types.InlineKeyboardMarkup()
markup.add(types.InlineKeyboardButton(text="Критические события", callback_data="notification_mode_disaster"))
markup.add(types.InlineKeyboardButton(text="Все события", callback_data="notification_mode_all"))
backend_bot.bot.send_message(chat_id,
"Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n"
'1. <b>Критические события</b> (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n'
'2. <b>Все события (По умолчанию)</b> - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.',
reply_markup=markup, parse_mode="HTML")
telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).")
@bot.callback_query_handler(func=lambda call: call.data.startswith("notification_mode_"))
def handle_notification_mode_selection(call):
chat_id = call.message.chat.id
message_id = call.message.message_id
mode = call.data.split("_")[2]
telebot.logger.debug(f"User ({chat_id}) selected notification mode: {mode}.")
# Убираем клавиатуру
backend_bot.bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None)
telebot.logger.debug(f"Removed inline keyboard for user ({chat_id}).")
# Обновляем режим уведомлений
disaster_only = True if mode == "disaster" else False
try:
telebot.logger.debug(f"Attempting to update notification mode in the database for user {chat_id}.")
with db_lock:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
query = 'UPDATE subscriptions SET disaster_only = ? WHERE chat_id = ?'
cursor.execute(query, (disaster_only, chat_id))
conn.commit()
mode_text = "Критические события" if disaster_only else "Все события"
backend_bot.bot.send_message(chat_id, f"Режим уведомлений успешно изменён на: {mode_text}")
telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}")
# Логируем изменение состояния пользователя
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
telebot.logger.debug(f"User state for {chat_id} set to SETTINGS_MENU.")
# Показываем меню настроек
show_settings_menu(chat_id)
telebot.logger.debug(f"Displayed settings menu to {chat_id}.")
except Exception as e:
telebot.logger.error(f"Error updating notification mode for {chat_id}: {e}")
backend_bot.bot.send_message(chat_id, "Произошла ошибка при изменении режима уведомлений.")
finally:
conn.close()
telebot.logger.debug(f"Database connection closed for user {chat_id}.")
# Логируем успешный ответ callback-запроса
bot.answer_callback_query(call.id)
telebot.logger.debug(f"Callback query for user ({chat_id}) answered.")
# Фаза 1: Запрос активных событий и выбор региона с постраничным переключением
def handle_active_triggers(message):
chat_id = message.chat.id
regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов
start_index = 0
markup = create_region_keyboard(regions, start_index)
backend_bot.bot.send_message(chat_id, "Выберите регион для получения активных событий:", reply_markup=markup)
def create_region_keyboard(regions, start_index, regions_per_page=10):
markup = types.InlineKeyboardMarkup()
end_index = min(start_index + regions_per_page, len(regions))
# Создаём кнопки для регионов
for i in range(start_index, end_index):
region_id, region_name = regions[i]
button = types.InlineKeyboardButton(text=f"{region_id}: {region_name}", callback_data=f"region_{region_id}")
markup.add(button)
# Добавляем кнопки для переключения страниц
navigation_row = []
if start_index > 0:
navigation_row.append(types.InlineKeyboardButton(text="<", callback_data=f"prev_{start_index}"))
if end_index < len(regions):
navigation_row.append(types.InlineKeyboardButton(text=">", callback_data=f"next_{end_index}"))
if navigation_row:
markup.row(*navigation_row)
markup.row(types.InlineKeyboardButton(text='Отмена', callback_data='cancel_active_triggers'))
return markup
@bot.callback_query_handler(
func=lambda call: call.data.startswith("region_") or call.data.startswith("prev_") or call.data.startswith(
"next_"))
def handle_region_pagination(call):
chat_id = call.message.chat.id
message_id = call.message.message_id
data = call.data
regions = bot_database.get_sorted_regions() # Используем функцию get_regions для получения регионов
regions_per_page = 10
# Если был выбран регион, то убираем клавиатуру и продолжаем выполнение функции
if data.startswith("region_"):
region_id = data.split("_")[1]
bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None)
handle_region_selection(call, region_id) # Продолжаем выполнение функции после выбора региона
# Если была нажата кнопка для переключения страниц
elif data.startswith("prev_") or data.startswith("next_"):
direction, index = data.split("_")
index = int(index)
# Рассчитываем новый индекс страницы
start_index = max(0, index - regions_per_page) if direction == "prev" else min(len(regions) - regions_per_page,
index)
# Обновляем клавиатуру для новой страницы
markup = create_region_keyboard(regions, start_index, regions_per_page)
bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=markup)
bot.answer_callback_query(call.id)
# Фаза 2: Обработка выбора региона и предложить выбор группы
def handle_region_selection(call, region_id):
chat_id = call.message.chat.id
try:
# Получаем группы хостов для выбранного региона
zapi = ZabbixAPI(ZABBIX_URL)
zapi.login(api_token=ZABBIX_API_TOKEN)
host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id})
filtered_groups = [group for group in host_groups if
'test' not in group['name'].lower() and f'_{region_id}' in group['name']]
# Если нет групп
if not filtered_groups:
backend_bot.bot.send_message(chat_id, "Нет групп хостов для этого региона.")
show_main_menu(chat_id)
return
# Создаем клавиатуру с выбором группы или всех групп
markup = types.InlineKeyboardMarkup()
for group in filtered_groups:
markup.add(types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}"))
markup.add(types.InlineKeyboardButton(text="Все группы региона\n(Долгое выполнение)",
callback_data=f"all_groups_{region_id}"))
backend_bot.bot.send_message(chat_id, "Выберите группу хостов или получите события по всем группам региона:",
reply_markup=markup)
except Exception as e:
backend_bot.bot.send_message(chat_id, f"Ошибка при подключении к Zabbix API.\n{str(e)}")
show_main_menu(chat_id)
# Фаза 3: Обработка выбора группы/всех групп и запрос периода
@bot.callback_query_handler(func=lambda call: call.data.startswith("group_") or call.data.startswith("all_groups_"))
def handle_group_or_all_groups(call):
chat_id = call.message.chat.id
message_id = call.message.message_id
# Убираем клавиатуру после выбора группы
bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None)
# Если выбрана конкретная группа
if call.data.startswith("group_"):
group_id = call.data.split("_")[1]
get_triggers_for_group(chat_id, group_id) # Сразу получаем события для группы
show_main_menu(chat_id)
# Если выбраны все группы региона
elif call.data.startswith("all_groups_"):
region_id = call.data.split("_")[2]
get_triggers_for_all_groups(chat_id, region_id) # Сразу получаем события для всех групп региона
show_main_menu(chat_id)
def run_polling():
bot.infinity_polling(timeout=10, long_polling_timeout=5)
# Запуск Flask-приложения
def run_flask():
app.run(port=5000, host='0.0.0.0', debug=True, use_reloader=False)
# Основная функция для запуска
def main():
# Инициализация базы данных
bot_database.init_db()
# Запуск Flask и бота в отдельных потоках
Thread(target=run_flask, daemon=True).start()
Thread(target=run_polling, daemon=True).start()
# Запуск асинхронных задач
asyncio.run(consume_from_queue())
if __name__ == '__main__':
main()