From dd66cb5712a052438592251eb9adf0a7f34a2125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=20=D0=97=D0=B2=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Mon, 9 Sep 2024 17:05:46 +0500 Subject: [PATCH] rework Active triggers function rework settings menu button add function to choose what severity level you want to receive --- .gitignore | 8 +- telezab.py | 779 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 534 insertions(+), 253 deletions(-) diff --git a/.gitignore b/.gitignore index e351bea..7652792 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,10 @@ /TODO.txt /logs /__pycache__ -/venv \ No newline at end of file +/venv +/app.log +/logs/ +/logs/app.log +/logs/error.log +/db/ +/db/telezab.db diff --git a/telezab.py b/telezab.py index 8fef4cd..662855e 100644 --- a/telezab.py +++ b/telezab.py @@ -1,9 +1,11 @@ import os -from flask import Flask, request, jsonify +from functools import partial +from flask import Flask, request, jsonify, render_template import schedule from dotenv import load_dotenv import hashlib import telebot +from telebot import types import logging from logging.config import dictConfig import zipfile @@ -24,6 +26,45 @@ import re # Load environment variables load_dotenv() + +# Функция для загрузки значения из файла +def load_value_from_file(file_name): + try: + with open(file_name, 'r') as file: + return file.read().strip() + except FileNotFoundError: + return None + + +# Функция для получения переменной из окружения или файла +def get_variable_value(variable_name): + # Попытка получить значение из окружения + value = os.getenv(variable_name) + + # Если переменная окружения не установлена, попробуем загрузить из файла + if not value: + file_value = "file_" + variable_name + value = os.getenv(file_value) + with open(value, 'r') as file: + value = file.read() + return value + return value + + +# Загрузка переменных окружения или значений из файлов +TOKEN = get_variable_value('TELEGRAM_TOKEN') +ZABBIX_API_TOKEN = get_variable_value('ZABBIX_API_TOKEN') +ZABBIX_URL = get_variable_value('ZABBIX_URL') + + +# Проверка наличия всех необходимых переменных +if not TOKEN or not ZABBIX_URL or not ZABBIX_API_TOKEN: + raise ValueError("One or more required environment variables are missing") + +os.makedirs('logs', exist_ok=True) +os.makedirs('db', exist_ok=True) + + class UTF8StreamHandler(logging.StreamHandler): def __init__(self, stream=None): super().__init__(stream) @@ -37,8 +78,9 @@ class UTF8StreamHandler(logging.StreamHandler): # Определение пути к основному лог-файлу LOG_FILE = 'logs/app.log' - - +ERROR_LOG_FILE = 'logs/error.log' +DB_PATH = 'db/telezab.db' +SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" # Определение функции архивирования логов def archive_old_logs(): @@ -59,7 +101,6 @@ def archive_old_logs(): os.remove(LOG_FILE) - class FilterByMessage(logging.Filter): def filter(self, record): # Фильтруем сообщения, содержащие 'Received 1 new updates' @@ -67,28 +108,41 @@ class FilterByMessage(logging.Filter): # Initialize Flask application -app = Flask(__name__) +app = Flask(__name__, template_folder='templates') # Настройка логирования dictConfig({ 'version': 1, + 'disable_existing_loggers': False, # Включаем, чтобы избежать дублирования логов 'formatters': { 'default': { 'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s', }, + 'error': { + 'format': '[%(asctime)s] %(levelname)s %(module)s: %(message)s', + }, }, 'handlers': { 'console': { - 'class': 'telezab.UTF8StreamHandler', # Замените на путь к вашему классу UTF8StreamHandler - 'stream': 'ext://sys.stdout', # Вывод в консоль + 'class': 'telezab.UTF8StreamHandler', + 'stream': 'ext://sys.stdout', 'formatter': 'default', 'filters': ['filter_by_message'] }, 'file': { 'class': 'logging.FileHandler', - 'filename': 'app.log', # Запись в файл + 'filename': 'logs/app.log', # Указываем путь к общим логам 'formatter': 'default', - 'encoding': 'utf-8', # Кодировка файла + 'encoding': 'utf-8', + 'filters': ['filter_by_message'] + }, + 'error_file': { + 'class': 'logging.FileHandler', + 'filename': 'logs/error.log', # Указываем путь к логам ошибок + 'formatter': 'error', + 'encoding': 'utf-8', + 'filters': ['filter_by_message'], + 'level': 'ERROR' # Логи ошибок будут записываться в отдельный файл }, }, 'filters': { @@ -98,19 +152,19 @@ dictConfig({ }, 'loggers': { 'flask': { - 'level': 'DEBUG', - 'handlers': ['console', 'file'], + 'level': 'WARNING', + 'handlers': ['file', 'error_file'], # Записываем логи во все файлы 'propagate': False, }, 'telebot': { - 'level': 'DEBUG', - 'handlers': ['console', 'file'], + 'level': 'WARNING', + 'handlers': ['file', 'error_file'], # Логи Telebot 'propagate': False, }, }, 'root': { - 'level': 'DEBUG', - 'handlers': ['console', 'file'], + 'level': 'WARNING', + 'handlers': ['file', 'error_file'], # Корневой логгер пишет в файлы } }) @@ -118,15 +172,6 @@ dictConfig({ app.logger.setLevel(logging.DEBUG) # Настройка pyTelegramBotAPI logger telebot.logger = logging.getLogger('telebot') -# Get the token from environment variables -TOKEN = os.getenv('TELEGRAM_TOKEN') -ZABBIX_URL = os.getenv('ZABBIX_URL') -ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') - -if not TOKEN or not ZABBIX_URL or not ZABBIX_API_TOKEN: - raise ValueError("One or more required environment variables are missing") - -ADMIN_CHAT_IDS = os.getenv('ADMIN_CHAT_IDS', '').split(',') bot = telebot.TeleBot(TOKEN) @@ -145,14 +190,12 @@ user_states = {} user_timers = {} - - def init_db(): global st st = datetime.now() try: with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # Create events table @@ -169,12 +212,19 @@ def init_db(): username TEXT, active BOOLEAN DEFAULT TRUE, skip BOOLEAN DEFAULT FALSE, + disaster_only BOOLEAN DEFAULT FALSE, UNIQUE(chat_id, region_id))''') # Create whitelist table cursor.execute('''CREATE TABLE IF NOT EXISTS whitelist ( - chat_id INTEGER PRIMARY KEY)''') + chat_id INTEGER PRIMARY KEY, + username TEXT, + user_email TEXT)''') + # Create whitelist table + cursor.execute('''CREATE TABLE IF NOT EXISTS admins ( + chat_id INTEGER PRIMARY KEY, + username TEXT)''') # Create regions table with active flag cursor.execute('''CREATE TABLE IF NOT EXISTS regions ( region_id TEXT PRIMARY KEY, @@ -213,7 +263,7 @@ def hash_data(data): # Check if user is in whitelist def is_whitelisted(chat_id): with db_lock: - conn = sqlite3.connect('telezab.db') + 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}") @@ -226,19 +276,32 @@ def is_whitelisted(chat_id): # Add user to whitelist def add_to_whitelist(chat_id, username): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() query = 'INSERT OR IGNORE INTO whitelist (chat_id, username) VALUES (?, ?)' telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, username={username}") - cursor.execute(query, (chat_id, 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() + query = 'INSERT OR IGNORE INTO whitelist (chat_id, username, user_email) VALUES (?, ?, ?)' + telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, username={username}, email={user_email}") + cursor.execute(query, (chat_id, username, user_email)) conn.commit() - conn.close() # Remove user from whitelist def remove_from_whitelist(chat_id): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() query = 'DELETE FROM whitelist WHERE chat_id = ?' telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}") @@ -246,11 +309,20 @@ def remove_from_whitelist(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 # Get list of regions def get_regions(): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute('SELECT region_id, region_name FROM regions WHERE active = TRUE ORDER BY region_id') regions = cursor.fetchall() @@ -260,7 +332,7 @@ def get_regions(): def get_sorted_regions(): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute('SELECT region_id, region_name FROM regions WHERE active = TRUE') regions = cursor.fetchall() @@ -274,7 +346,7 @@ def get_sorted_regions(): # Check if region exists def region_exists(region_id): with db_lock: - conn = sqlite3.connect('telezab.db') + 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] @@ -285,7 +357,7 @@ def region_exists(region_id): # Get list of regions a user is subscribed to def get_user_subscribed_regions(chat_id): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(''' SELECT regions.region_id, regions.region_name @@ -302,7 +374,7 @@ def get_user_subscribed_regions(chat_id): # Check if user is subscribed to a region def is_subscribed(chat_id, region_id): with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute(''' SELECT COUNT(*) @@ -323,7 +395,7 @@ def log_user_event(chat_id, username, action): timestamp = time.strftime('%Y-%m-%d %H:%M:%S') try: with db_lock: - conn = sqlite3.connect('telezab.db') + 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}") @@ -348,7 +420,7 @@ def set_user_state(chat_id, state): def start_settings_timer(chat_id): if chat_id in user_timers: user_timers[chat_id].cancel() - timer = Timer(30, transition_to_notification_mode, [chat_id]) + timer = Timer(300, transition_to_notification_mode, [chat_id]) user_timers[chat_id] = timer timer.start() @@ -382,39 +454,37 @@ def show_main_menu(chat_id): bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) +def create_settings_keyboard(chat_id, admins_list): + markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) + # Линия 1: "Подписаться", "Отписаться" + markup.row('Подписаться','Отписаться','Мои подписки') + markup.row('Активные регионы','Режим уведомлений') + if chat_id in admins_list: + markup.row('Добавить регион', 'Удалить регион') + markup.row('Назад') + return markup + + # Settings menu for users def show_settings_menu(chat_id): - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - if str(chat_id) in ADMIN_CHAT_IDS: - markup.add('Подписаться', 'Отписаться', 'Мои подписки', 'Активные регионы', 'Добавить регион', 'Удалить регион', 'Назад') - markup.add('Тестовое событие', 'Тест триггеров') - else: - markup.add('Подписаться', 'Отписаться', 'Мои подписки', 'Активные регионы', 'Назад') + if not is_whitelisted(chat_id): + bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") + return + + admins_list = get_admins() + markup = create_settings_keyboard(chat_id, admins_list) + bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup) -# Handle /start command -@bot.message_handler(commands=['start']) -def handle_start(message): - chat_id = message.chat.id - username = message.from_user.username - if username: - username = f"@{username}" - else: - username = "N/A" - - set_user_state(chat_id, NOTIFICATION_MODE) - show_main_menu(chat_id) - - telebot.logger.info(f"User {chat_id} ({username}) started with command /start.") - - # Handle menu button presses @bot.message_handler(func=lambda message: True) def handle_menu_selection(message): chat_id = message.chat.id text = message.text.strip().lower() - + if not is_whitelisted(chat_id) and text not in ['регистрация', '/start']: + bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.") + return if user_states.get(chat_id, NOTIFICATION_MODE) == SETTINGS_MODE: reset_settings_timer(chat_id) handle_settings_menu_selection(message) @@ -437,9 +507,12 @@ def handle_menu_selection(message): def handle_settings_menu_selection(message): chat_id = message.chat.id text = message.text.strip().lower() + if not is_whitelisted(chat_id): + bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.") + return reset_settings_timer(chat_id) - + admins_list = get_admins() if text == 'подписаться': handle_subscribe(message) elif text == 'отписаться': @@ -448,13 +521,15 @@ def handle_settings_menu_selection(message): handle_my_subscriptions(message) elif text == 'активные регионы': handle_active_regions(message) - elif text == 'добавить регион' and str(chat_id) in ADMIN_CHAT_IDS: + elif text == 'режим уведомлений': + handle_notification_mode(message) + elif text == 'добавить регион' and chat_id in admins_list: prompt_admin_for_region(chat_id, 'add') - elif text == 'удалить регион' and str(chat_id) in ADMIN_CHAT_IDS: + elif text == 'удалить регион' and chat_id in admins_list: prompt_admin_for_region(chat_id, 'remove') - elif text == 'тестовое событие' and str(chat_id) in ADMIN_CHAT_IDS: + elif text == 'тестовое событие' and chat_id in admins_list: simulate_event(message) - elif text == 'тест триггеров' and str(chat_id) in ADMIN_CHAT_IDS: + elif text == 'тест триггеров' and chat_id in admins_list: simulate_triggers(message) elif text == 'назад': set_user_state(chat_id, NOTIFICATION_MODE) @@ -493,15 +568,17 @@ def process_subscription(message, chat_id, username): valid_region_ids = get_regions() valid_region_ids = [region[0] for region in valid_region_ids] with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() for region_id in region_ids: region_id = region_id.strip() if not region_id.isdigit() or region_id not in valid_region_ids: - bot.send_message(chat_id, f"Регион с ID {region_id} не существует или недопустимый формат. Введите только существующие номера регионов.") + bot.send_message(chat_id, + f"Регион с ID {region_id} не существует или недопустимый формат. Введите только существующие номера регионов.") return show_settings_menu(chat_id) query = 'INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username, active) VALUES (?, ?, ?, TRUE)' - telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, region_id={region_id}, username={username}") + telebot.logger.debug( + f"Executing query: {query} with chat_id={chat_id}, region_id={region_id}, username={username}") cursor.execute(query, (chat_id, region_id, username)) if cursor.rowcount == 0: query = 'UPDATE subscriptions SET active = TRUE WHERE chat_id = ? AND region_id = ?' @@ -544,7 +621,7 @@ def process_unsubscription(message, chat_id): valid_region_ids = get_user_subscribed_regions(chat_id) valid_region_ids = [region[0] for region in valid_region_ids] with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() for region_id in region_ids: region_id = region_id.strip() @@ -566,17 +643,22 @@ def process_unsubscription(message, chat_id): # Handle /help command to provide instructions @bot.message_handler(commands=['help']) def handle_help(message): + chat_id = message.chat.id + if not is_whitelisted(chat_id): + bot.send_message(chat_id, "Вы неавторизованы для использования этого бота.") + return help_text = ( - "/start - Показать меню бота\n" - "Настройки - Перейти в режим настроек и управлять подписками\n" - "Помощь - Показать это сообщение" + '/start - Показать меню бота\n' + 'Настройки - Перейти в режим настроек и управления подписками\n' + 'Активные тригеры - Получение активных проблем за последние 24 часа\n' + 'Помощь - Описание всех возможностей бота' + ) - bot.send_message(message.chat.id, help_text) + bot.send_message(message.chat.id, help_text, parse_mode="html") show_main_menu(message.chat.id) # Handle /register command for new user registration -@bot.message_handler(commands=['register']) def handle_register(message): chat_id = message.chat.id username = message.from_user.username @@ -586,60 +668,9 @@ def handle_register(message): username = "N/A" bot.send_message(chat_id, - f"Ваш chat ID: {chat_id}, ваше имя пользователя: {username}. Запрос на одобрение отправлен администратору.") + f"Ваш chat ID: {chat_id}\nВаше имя пользователя: {username}\nДля продолжения регистрации необходимо отправить письмо на почту {SUPPORT_EMAIL}\nВ теме письма указать Telegram Bot\nВ теле письма указать полученные данные") log_user_event(chat_id, username, "Requested registration") - for admin_chat_id in ADMIN_CHAT_IDS: - markup = telebot.types.InlineKeyboardMarkup() - markup.add( - telebot.types.InlineKeyboardButton(text="Подтвердить", callback_data=f"approve_{chat_id}_{username}"), - telebot.types.InlineKeyboardButton(text="Отменить", callback_data=f"decline_{chat_id}_{username}") - ) - bot.send_message( - admin_chat_id, - f"Пользователь {username} ({chat_id}) запрашивает регистрацию. Вы подтверждаете это действие?", - reply_markup=markup - ) - - -@bot.callback_query_handler(func=lambda call: call.data.startswith("approve_") or call.data.startswith("decline_")) -def handle_admin_response(call): - action, chat_id, username = call.data.split("_") - if action == "approve": - add_to_whitelist(int(chat_id), username) - bot.send_message(chat_id, "Ваша регистрация одобрена администратором.") - bot.send_message(call.message.chat.id, f"Пользователь {username} ({chat_id}) добавлен в белый список.") - log_user_event(chat_id, username, "Approved registration") - elif action == "decline": - bot.send_message(chat_id, "Ваша регистрация отклонена администратором.") - bot.send_message(call.message.chat.id, f"Пользователь {username} ({chat_id}) отклонен.") - log_user_event(chat_id, username, "Declined registration") - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None) - bot.answer_callback_query(call.id) - - - -def process_register(message, chat_id, username): - if message.text.lower() == 'отмена': - bot.send_message(chat_id, "Регистрация отменена.") - show_main_menu(chat_id) - return - - if message.text.lower() == 'подтвердить регистрацию': - for admin_chat_id in ADMIN_CHAT_IDS: - markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) - markup.add(f'/add_whitelist {chat_id}', 'Отмена') - bot.send_message( - admin_chat_id, - f"Пользователь {username} ({chat_id}) запрашивает регистрацию.\n" - f"Вы подтверждаете это действие?", - reply_markup=markup - ) - bot.send_message(chat_id, "Запрос отправлен администратору для одобрения.") - telebot.logger.info(f"User {chat_id} ({username}) requested registration.") - else: - bot.send_message(chat_id, "Некорректный выбор. Регистрация отменена.") - show_main_menu(chat_id) # Handle admin region management commands @@ -658,7 +689,7 @@ def process_add_region(message): try: region_id, region_name = message.text.split()[0], ' '.join(message.text.split()[1:]) with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() query = 'SELECT region_name, active FROM regions WHERE region_id = ?' telebot.logger.debug(f"Executing query: {query} with region_id={region_id}") @@ -705,7 +736,7 @@ def handle_region_action(call): return show_settings_menu(chat_id) with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() if action == "replace": query = 'UPDATE regions SET region_name = ?, active = TRUE WHERE region_id = ?' @@ -733,7 +764,7 @@ def process_remove_region(message): try: region_id = message.text.split()[0] with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() # Проверка существования региона query = 'SELECT COUNT(*) FROM regions WHERE region_id = ?' @@ -757,6 +788,7 @@ def process_remove_region(message): bot.send_message(chat_id, "Неверный формат. Используйте: ") show_settings_menu(chat_id) + # Handle displaying active subscriptions for a user def handle_my_subscriptions(message): chat_id = message.chat.id @@ -790,15 +822,25 @@ def handle_active_regions(message): regions_list = format_regions_list(regions) bot.send_message(chat_id, f"Активные регионы:\n{regions_list}") show_settings_menu(chat_id) - - # RabbitMQ configuration RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost') RABBITMQ_QUEUE = 'telegram_notifications' def rabbitmq_connection(): - connection = pika.BlockingConnection(pika.ConnectionParameters(RABBITMQ_HOST)) + # Создаем объект учетных данных + credentials = pika.PlainCredentials('admin', 'admin') + + # Указываем параметры подключения, включая учетные данные + 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 @@ -839,7 +881,14 @@ async def send_message(chat_id, message, is_notification=False): try: if is_notification: await rate_limit_semaphore.acquire() - await run_in_executor(bot.send_message, chat_id, message) + parse_mode = 'HTML' + + # Используем partial для передачи именованных аргументов в bot.send_message + func_with_args = partial(bot.send_message, chat_id=chat_id, text=message, parse_mode=parse_mode) + + # Передаем подготовленную функцию в run_in_executor + await run_in_executor(func_with_args) + except telebot.apihelper.ApiTelegramException as e: if "429" in str(e): telebot.logger.warning(f"Rate limit exceeded for chat_id {chat_id}. Retrying...") @@ -847,14 +896,16 @@ async def send_message(chat_id, message, is_notification=False): await send_message(chat_id, message, is_notification) 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: - telebot.logger.error(f"Error sending message to {chat_id}: {e}") + telebot.logger.error(f"Unexpected error while sending message to {chat_id}: {e}", exc_info=True) await check_telegram_api() finally: if is_notification: rate_limit_semaphore.release() + async def send_notification_message(chat_id, message): await send_message(chat_id, message, is_notification=True) @@ -894,7 +945,7 @@ def webhook(): event_hash = hash_data(data) with db_lock: - conn = sqlite3.connect('telezab.db') + conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM events') count = cursor.fetchone()[0] @@ -910,12 +961,17 @@ def webhook(): app.logger.error(f"Failed to extract region number from host: {data.get('host')}") return jsonify({"status": "error", "message": "Invalid host format"}), 400 - # Fetch chat_ids to send the alert - query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE AND skip = FALSE' + # Fetch chat_ids to send the alert based on disaster_only flag + if data['severity'] == '5': # Авария + 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"Executing query: {query} with region_id={region_id}") cursor.execute(query, (region_id,)) results = cursor.fetchall() + # Check if the region is active query = 'SELECT active FROM regions WHERE region_id = ?' cursor.execute(query, (region_id,)) @@ -967,40 +1023,159 @@ def format_message(data): if data['status'].upper() == "PROBLEM": message = ( - f"⚠️ {data['host']} ({data["ip"]})\n" + f"⚠️ {data['host']} ({data['ip']})\n" f"{data['msg']}\n" f"Критичность: {data['severity']}" ) if 'link' in data: - message += f'\nURL: {data['link']}' + message += f'\nURL: Ссылка на график' return message else: message = ( - f"✅ {data['host']} ({data["ip"]})\n" - f"{data['msg']}\n" - f"Критичность: {data['severity']}\n" - f"Проблема устранена!\n" - f"Время устранения проблемы: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))}" + f"✅ {data['host']} ({data['ip']})\n" + f"Описание: {data['msg']}\n" + f"Критичность: {data['severity']}\n" + f"Проблема устранена!\n" + f"Время устранения проблемы: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))}\n" ) + if 'link' in data: + message += f'URL: Ссылка на график' return message except KeyError as e: app.logger.error(f"Missing key in data: {e}") raise ValueError(f"Missing key in data: {e}") +@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] + + # Убираем клавиатуру + bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) + + # Обновляем режим уведомлений + disaster_only = True if mode == "disaster" else False + + try: + 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 "Все события" + bot.send_message(chat_id, f"Режим уведомлений успешно изменён на: {mode_text}") + except Exception as e: + telebot.logger.error(f"Error updating notification mode for {chat_id}: {e}") + bot.send_message(chat_id, "Произошла ошибка при изменении режима уведомлений.") + finally: + conn.close() + + bot.answer_callback_query(call.id) + +@bot.message_handler(func=lambda message: message.text.lower() == 'режим уведомлений') +def handle_notification_mode(message): + chat_id = message.chat.id + if not is_whitelisted(chat_id): + bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") + return + + markup = types.InlineKeyboardMarkup() + markup.add(types.InlineKeyboardButton(text="Все события", callback_data="notification_mode_all")) + markup.add(types.InlineKeyboardButton(text="Только Авария", callback_data="notification_mode_disaster")) + + bot.send_message(chat_id, "Выберите какой режим уведомлений вы хотите:\n" + "1. Все события - В этом режиме вы получаете все события с критическим уровнем 'Высокая' и 'Авария'.\n" + "2. Авария - В этом режиме вы получаете только события уровня 'Авария'.", reply_markup=markup) + + @app.route('/add_user', 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') - if telegram_id and chat_id: - add_to_whitelist(chat_id, telegram_id) + if telegram_id and chat_id and user_email: + rundeck_add_to_whitelist(chat_id, telegram_id, user_email) app.logger.info(f"User {telegram_id} added to whitelist.") - return jsonify({"status": "success"}), 200 + bot.send_message(chat_id, "Регистрация пройдена успешна.") + return jsonify({"status": "success","msg": f"User {telegram_id} with {user_email} added successfull"}), 200 else: app.logger.error("Invalid data received for adding user.") return jsonify({"status": "failure", "reason": "Invalid data"}), 400 +#Получение информации по текущим юзерам +@app.route('/users/get_users', methods=['GET']) +def get_users(): + try: + with db_lock: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Получение пользователей из таблицы whitelist + cursor.execute('SELECT * FROM whitelist') + users = cursor.fetchall() + users_dict = {id: {'id': id, 'username': username, 'email': email, 'events': [], 'worker': '', 'subscriptions': []} + for id, username, email in users} + + # Получение событий из таблицы user_events + cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') + events = cursor.fetchall() + + # Обработка событий и добавление их в соответствующего пользователя + 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) + + # Получение активных подписок из таблицы subscription + cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') + subscriptions = cursor.fetchall() + + # Добавление подписок к соответствующему пользователю + for chat_id, region_id in subscriptions: + if chat_id in users_dict: + users_dict[chat_id]['subscriptions'].append(str(region_id)) + + # Формирование worker из email (Имя Фамилия) + for user in users_dict.values(): + email = user['email'] + name_parts = email.split('@')[0].split('.') + if len(name_parts) >= 2: + user['worker'] = f"{name_parts[0].capitalize()} {name_parts[1].capitalize()}" + if len(name_parts) == 3: + user['worker'] += f" {name_parts[2][0].capitalize()}." + + conn.commit() + conn.close() + + # Преобразование в список с упорядоченными ключами + 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) + + return jsonify(result) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/users/view', methods=['GET']) +def view_users(): + # noinspection PyUnresolvedReferences + return render_template('users.html') # Обработчик для переключения уровня логирования Flask @@ -1044,141 +1219,252 @@ def toggle_telebot_debug(): except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 -# Handle active triggers +# Фаза 1: Запрос активных триггеров и выбор региона с постраничным переключением def handle_active_triggers(message): chat_id = message.chat.id - regions = get_user_subscribed_regions(chat_id) - regions_per_page = 3 + regions = get_user_subscribed_regions(chat_id) # Функция для получения доступных регионов + if not regions: + bot.send_message(chat_id, "У вас нет подписанных регионов.") + return + start_index = 0 - - markup = create_region_markup(regions, start_index, regions_per_page) - bot.send_message(chat_id, "По какому региону хотите получить активные проблемы:", reply_markup=markup) + markup = create_region_keyboard(regions, start_index) + bot.send_message(chat_id, "Выберите регион для получения активных триггеров:", reply_markup=markup) -def create_region_markup(regions, start_index, regions_per_page): - markup = telebot.types.InlineKeyboardMarkup() +def create_region_keyboard(regions, start_index, regions_per_page=5): + markup = types.InlineKeyboardMarkup() end_index = min(start_index + regions_per_page, len(regions)) - buttons = [] + # Создаём кнопки для регионов for i in range(start_index, end_index): region_id, region_name = regions[i] - buttons.append(telebot.types.InlineKeyboardButton(text=region_id, callback_data=f"region_{region_id}")) + button = types.InlineKeyboardButton(text=f"{region_name}", callback_data=f"region_{region_id}") + markup.add(button) - prev_button = telebot.types.InlineKeyboardButton(text="<", callback_data=f"prev_{start_index}") if start_index > 0 else None - next_button = telebot.types.InlineKeyboardButton(text=">", callback_data=f"next_{start_index}") if end_index < len(regions) else None + # Добавляем кнопки для переключения страниц + 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}")) - row_buttons = [prev_button] + buttons + [next_button] - row_buttons = [btn for btn in row_buttons if btn is not None] # Remove None values + if navigation_row: + markup.row(*navigation_row) - markup.row(*row_buttons) return markup -@bot.callback_query_handler(func=lambda call: call.data.startswith("region_")) -def handle_region_selection(call): - region_id = call.data.split("_")[1] +@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 + + # Убираем клавиатуру, если регион был выбран + if data.startswith("region_"): + bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) + + regions = get_user_subscribed_regions(chat_id) + regions_per_page = 5 + + if data.startswith("region_"): + region_id = data.split("_")[1] + 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, call.message.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: - # Получение всех групп хостов, содержащих region_id в названии + # Получаем группы хостов для выбранного региона zapi = ZabbixAPI(ZABBIX_URL) zapi.login(api_token=ZABBIX_API_TOKEN) - host_groups = zapi.hostgroup.get( - output=["groupid", "name"], - search={"name": region_id} - ) - - # Фильтрация групп хостов, исключая те, в названии которых есть "test" + 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()] + # Если нет групп if not filtered_groups: - bot.send_message(chat_id, "Нет групп хостов, соответствующих данному региону.") - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None) - bot.answer_callback_query(call.id) + bot.send_message(chat_id, "Нет групп хостов для этого региона.") + return_to_main_menu(chat_id) return - # Отправка списка групп хостов пользователю в виде кнопок - markup = telebot.types.InlineKeyboardMarkup() + # Создаем клавиатуру с выбором группы или всех групп + markup = types.InlineKeyboardMarkup() for group in filtered_groups: - markup.add(telebot.types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) + markup.add(types.InlineKeyboardButton(text=group['name'], callback_data=f"group_{group['groupid']}")) + markup.add(types.InlineKeyboardButton(text="Все группы региона", callback_data=f"all_groups_{region_id}")) - bot.send_message(chat_id, f"Найдены следующие группы хостов для региона {region_id}:", reply_markup=markup) + bot.send_message(chat_id, "Выберите группу хостов или получите триггеры по всем группам региона:", reply_markup=markup) except Exception as e: - telebot.logger.error(f"Error connecting to Zabbix API: {e}") - bot.send_message(chat_id, "Не удалось подключиться к Zabbix API. Пожалуйста, попробуйте позже.") + bot.send_message(chat_id, "Ошибка при подключении к Zabbix API.") + return_to_main_menu(chat_id) - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None) - bot.answer_callback_query(call.id) -@bot.callback_query_handler(func=lambda call: call.data.startswith("group_")) -def handle_group_selection(call): - group_id = call.data.split("_")[1] +# Фаза 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 - try: - # Получение триггеров для выбранной группы хостов - triggers = get_zabbix_triggers(group_id) - if triggers is None: - bot.send_message(chat_id, "Не удалось подключиться к Zabbix API. Пожалуйста, попробуйте позже.") - elif not triggers: - bot.send_message(chat_id, "Нет активных проблем по указанной группе уровня HIGH и DISASTER за последние 24 часа.") - else: - for trigger in triggers: - bot.send_message(chat_id, trigger, parse_mode="html") - time.sleep(1/5) - except Exception as e: - telebot.logger.error(f"Error processing group selection: {e}") - bot.send_message(chat_id, "Произошла ошибка при обработке вашего запроса. Пожалуйста, попробуйте позже.") + # Убираем клавиатуру после выбора группы + bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None) - bot.answer_callback_query(call.id) + # Если выбрана конкретная группа + if call.data.startswith("group_"): + group_id = call.data.split("_")[1] + get_triggers_for_group(chat_id, group_id) # Сразу получаем триггеры для группы + + # Если выбраны все группы региона + elif call.data.startswith("all_groups_"): + region_id = call.data.split("_")[2] + get_triggers_for_all_groups(chat_id, region_id) # Сразу получаем триггеры для всех групп региона -@bot.callback_query_handler(func=lambda call: call.data.startswith("prev_") or call.data.startswith("next_")) -def handle_pagination(call): - direction, index = call.data.split("_") - index = int(index) - regions = get_user_subscribed_regions(call.message.chat.id) - regions_per_page = 3 - if direction == "prev": - start_index = max(0, index - regions_per_page) +# # Фаза 4: Обработка выбора периода и отправка триггеров +# @bot.callback_query_handler(func=lambda call: call.data.startswith("period_")) +# def handle_period_selection(call): +# chat_id = call.message.chat.id +# message_id = call.message.message_id +# data = call.data +# +# # Убираем клавиатуру, чтобы избежать повторных срабатываний +# bot.edit_message_reply_markup(chat_id=chat_id, message_id=message_id, reply_markup=None) +# +# +# # Извлекаем количество часов и оставшуюся часть данных +# period_prefix, period_hours, group_or_all_data = data.split("_", 2) +# +# try: +# # Преобразуем количество часов в целое число +# hours = int(period_hours) +# +# # Определяем, что было выбрано (группа или все группы региона) +# if group_or_all_data.startswith("group_"): +# group_id = group_or_all_data.split("_")[1] +# get_triggers_for_group(chat_id, group_id) +# elif group_or_all_data.startswith("all_groups_"): +# region_id = group_or_all_data.split("_")[2] +# get_triggers_for_all_groups(chat_id, region_id) +# except ValueError as e: +# bot.send_message(chat_id, "Произошла ошибка при выборе периода. Попробуйте снова.") +# telebot.logger.error(f"Error processing period selection: {e}") + + + +# Вспомогательная функция: получение триггеров для группы +def get_triggers_for_group(chat_id, group_id): + triggers = get_zabbix_triggers(group_id) # Получаем все активные триггеры без периода + if not triggers: + bot.send_message(chat_id, f"Нет активных триггеров.") else: - start_index = min(len(regions) - regions_per_page, index + regions_per_page) + send_triggers_to_user(triggers, chat_id) - markup = create_region_markup(regions, start_index, regions_per_page) - bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=markup) - bot.answer_callback_query(call.id) # Завершение обработки callback +def get_triggers_for_all_groups(chat_id, region_id): + try: + zapi = ZabbixAPI(ZABBIX_URL) + zapi.login(api_token=ZABBIX_API_TOKEN) + + host_groups = zapi.hostgroup.get(output=["groupid", "name"], search={"name": region_id}) + filtered_groups = [group for group in host_groups if 'test' not in group['name'].lower()] + + all_triggers = [] + for group in filtered_groups: + triggers = get_zabbix_triggers(group['groupid']) + if triggers: + all_triggers.extend(triggers) + + if all_triggers: + send_triggers_to_user(all_triggers, chat_id) + else: + bot.send_message(chat_id, f"Нет активных триггеров.") + except Exception as e: + bot.send_message(chat_id, "Ошибка при получении триггеров.") + return_to_main_menu(chat_id) + + + +# Вспомогательная функция: отправка триггеров пользователю +def send_triggers_to_user(triggers, chat_id): + for trigger in triggers: + bot.send_message(chat_id, trigger, parse_mode="html") + time.sleep(1 / 5) + + +# Вспомогательная функция: возврат в главное меню +def return_to_main_menu(chat_id): + markup = types.ReplyKeyboardMarkup(resize_keyboard=True) + markup.add(types.KeyboardButton("Вернуться в меню")) + bot.send_message(chat_id, "Вы можете вернуться в главное меню.", reply_markup=markup) + + +def escape_telegram_chars(text): + """ + Экранирует запрещённые символы для Telegram API: + < -> < + > -> > + & -> & + Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием. + """ + replacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', # Для кавычек + } + + # Применяем замены + for char, replacement in replacements.items(): + text = text.replace(char, replacement) + + return text def get_zabbix_triggers(group_id): try: - # Создайте подключение к вашему Zabbix серверу zapi = ZabbixAPI(ZABBIX_URL) zapi.login(api_token=ZABBIX_API_TOKEN) - # Получение триггеров уровня "Высокая" и "Авария" за последние 24 часа для указанной группы хостов - time_from = int(time.time()) - 24 * 3600 # последние 24 часа - time_till = int(time.time()) + # Рассчитываем временной диапазон на основе переданного количества часов + # time_from = int(time.time()) - period * 3600 # последние N часов + # time_till = int(time.time()) + + telebot.logger.info(f"Fetching triggers for group {group_id}") + + # Получение триггеров triggers = zapi.trigger.get( output=["triggerid", "description", "priority"], selectHosts=["hostid", "name"], groupids=group_id, - filter={"priority": ["4", "5"], "value": "1"}, + filter={"priority": ["4", "5"], "value": "1"}, # Высокий приоритет и авария only_true=1, active=1, withLastEventUnacknowledged=1, - time_from=time_from, - time_till=time_till, + # time_from=time_from, # Устанавливаем временной фильтр + # time_till=time_till, expandDescription=1, expandComment=1, selectItems=["itemid", "lastvalue"], selectLastEvent=["clock"] ) + telebot.logger.info(f"Found {len(triggers)} triggers for group {group_id}") + # Московское время moskva_tz = timezone('Europe/Moscow') @@ -1188,30 +1474,24 @@ def get_zabbix_triggers(group_id): } trigger_messages = [] - current_time = datetime.now(moskva_tz) - one_day_ago = current_time - timedelta(days=1) for trigger in triggers: - # Получаем время последнего события event_time_epoch = int(trigger['lastEvent']['clock']) event_time = datetime.fromtimestamp(event_time_epoch, tz=moskva_tz) - # Проверяем, не старше ли событие чем на сутки - if event_time < one_day_ago: - continue - - description = trigger['description'] + description = escape_telegram_chars(trigger['description']) host = trigger['hosts'][0]['name'] priority = priority_map.get(trigger['priority'], 'Неизвестно') - - # Генерация ссылки на график + # Получаем itemids item_ids = [item['itemid'] for item in trigger['items']] - graph_links = [] - for item_id in item_ids: - graph_link = f'Ссылка на график' - graph_links.append(graph_link) - graph_links_str = "\n".join(graph_links) + + telebot.logger.info(f"Trigger {trigger['triggerid']} on host {host} has itemids: {item_ids}") + + # Формируем ссылку для batchgraph + batchgraph_link = f"{ZABBIX_URL}/history.php?action=batchgraph&" + batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids]) + batchgraph_link += "&graphtype=0" # Заменяем {HOST.NAME} на имя хоста description = description.replace("{HOST.NAME}", host) @@ -1228,21 +1508,16 @@ def get_zabbix_triggers(group_id): f"Критичность: {priority}\n" f"Описание: {description}\n" f"Время создания: {event_time_formatted}\n" - f"{graph_links_str}") + # f"ItemIDs: {', '.join(item_ids)}\n" + f'URL: Ссылка на график') trigger_messages.append(message) return trigger_messages except Exception as e: - telebot.logger.error(f"Error connecting to Zabbix API: {e}") + telebot.logger.error(f"Error fetching triggers for group {group_id}: {e}") return None - -async def send_group_messages(chat_id, messages): - for message in messages: - await send_message(chat_id, message, is_notification=True) - await asyncio.sleep(1) # Delay between messages to comply with rate limit - # Test functions for admin def simulate_event(message): chat_id = message.chat.id @@ -1255,7 +1530,6 @@ def simulate_event(message): "status": "Авария!" } - app.logger.info(f"Simulating event: {test_event}") # Use requests to simulate a POST request response = requests.post('http://localhost:5000/webhook', json=test_event) @@ -1279,7 +1553,8 @@ def simulate_triggers(message): def run_polling(): - bot.polling(non_stop=True, interval=0) + bot.infinity_polling(timeout=10, long_polling_timeout = 5) + # Запуск Flask-приложения def run_flask(): @@ -1293,11 +1568,11 @@ def schedule_jobs(): schedule.run_pending() time.sleep(60) # Проверять раз в минуту + # Основная функция для запуска def main(): # Инициализация базы данных init_db() - print('Bootstrap wait...') # Запуск Flask и бота в отдельных потоках Thread(target=run_flask, daemon=True).start() @@ -1307,6 +1582,6 @@ def main(): # Запуск асинхронных задач asyncio.run(consume_from_queue()) -if __name__ == '__main__': - main() +if __name__ == '__main__': + main() \ No newline at end of file