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 = (
'/start - Показать меню бота\n'
'Настройки - Перейти в режим настроек и управления подписками\n'
'Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
'Помощь - Описание всех возможностей бота'
)
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'В теме письма указать "Подтверждение регистрации в телеграм-боте TeleZab".\n'
f'В теле письма указать:\n'
f'1. ФИО\n'
f'2. Ваш Chat ID: {chat_id}\n'
f'3. Ваше имя пользователя: {username}')
backend_bot.bot.send_message(chat_id, text, parse_mode="HTML")
bot_database.log_user_event(chat_id, username, "Requested registration")
# Handle /start command
@bot.message_handler(commands=['start'])
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. Критические события (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n'
'2. Все события (По умолчанию) - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.',
reply_markup=markup, parse_mode="HTML")
telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).")
@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()