import os
from functools import partial
from flask import Flask, request, jsonify, render_template
from dotenv import load_dotenv
import hashlib
import telebot
from telebot import types
import logging
from threading import Thread, Lock
import sqlite3
import time
import asyncio
import aiohttp
import pika
import aio_pika
import json
from concurrent.futures import ThreadPoolExecutor
from pyzabbix import ZabbixAPI
import requests
from pytz import timezone
from datetime import datetime
import re
import urllib.parse
from log_manager import LogManager
# from rabbitmq_worker import RABBITMQ_LOGIN
from region_api import RegionAPI
from user_state_manager import UserStateManager
# Load environment variables
load_dotenv()
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 configuration
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost')
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}/"
# Инициализируем класс RegionApi
region_api = RegionAPI(DB_PATH)
# Инициализируем класс UserStateManager
user_state_manager = UserStateManager()
# Initialize Flask application
app = Flask(__name__,static_url_path='/telezab/static', template_folder='templates')
# Инициализация LogManager
log_manager = LogManager(log_dir='logs', retention_days=30)
# Настройка уровня логирования для Flask
app.logger.setLevel(logging.INFO)
# Настройка pyTelegramBotAPI logger
telebot.logger = logging.getLogger('telebot')
# Важно: вызов schedule_log_rotation для планировки ротации и архивации логов
log_manager.schedule_log_rotation()
bot = telebot.TeleBot(TOKEN)
# Lock for database operations
db_lock = Lock()
# Semaphore for rate limiting
rate_limit_semaphore = asyncio.Semaphore(25) # 25 messages per second
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()
# Hash the incoming data
def hash_data(data):
return hashlib.sha256(str(data).encode('utf-8')).hexdigest()
# Check if user is in whitelist
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
# Add user to whitelist
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 # Успешное добавление
# Remove user from whitelist
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
# Check if region exists
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
# Get list of regions a user is subscribed to
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
# Check if user is subscribed to a region
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
# Format regions list
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()
# Define states
NOTIFICATION_MODE = 1
SETTINGS_MODE = 2
# 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'
'Активные события - Получение всех нерешённых событий мониторинга по выбранным сервисам выбранного региона\n'
'Помощь - Описание всех возможностей бота'
)
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}')
bot.send_message(chat_id,text,parse_mode="HTML")
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)
def show_main_menu(chat_id):
markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
if is_whitelisted(chat_id):
user_state_manager.set_state(chat_id, "MAIN_MENU")
markup.add('Настройки', 'Помощь', 'Активные события')
else:
user_state_manager.set_state(chat_id, "REGISTRATION")
markup.add('Регистрация')
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 DEV == '1':
if chat_id in admins_list:
markup.row('Активные регионы')
markup.row('Добавить регион', 'Удалить регион')
markup.row('Назад')
return markup
# Settings menu for users
def show_settings_menu(chat_id):
if not is_whitelisted(chat_id):
user_state_manager.set_state(chat_id, "REGISTRATION")
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)
# Основной обработчик меню
@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 is_whitelisted(chat_id) and text != 'Регистрация':
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
return
# Получаем текущее состояние пользователя
current_state = user_state_manager.get_state(chat_id)
# Обработка команд в зависимости от состояния
if current_state == "MAIN_MENU":
handle_main_menu(message, chat_id, text)
elif current_state == "REGISTRATION":
handle_register(message)
elif current_state == "SETTINGS_MENU":
handle_settings_menu(message, chat_id, text)
elif current_state == "SUBSCRIBE":
process_subscription_button(message, chat_id, username)
elif current_state == "UNSUBSCRIBE":
process_unsubscription_button(message, chat_id, username)
# elif current_state == "ADD_REGION":
# process_add_region_button(message, chat_id, username)
# elif current_state == "REMOVE_REGION":
# process_remove_region_button(message, chat_id, username)
else:
bot.send_message(chat_id, "Команда не распознана.")
show_main_menu(chat_id)
def handle_main_menu(message, chat_id, text):
"""Обработка команд в главном меню."""
if text == 'Регистрация':
user_state_manager.set_state(chat_id, "REGISTRATION")
handle_register(message)
elif text == 'Настройки':
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
elif text == 'Помощь':
handle_help(message)
elif text == 'Активные события':
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() == 'подписаться':
user_state_manager.set_state(chat_id, "SUBSCRIBE")
handle_subscribe_button(message)
elif text.lower() == 'отписаться':
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() == 'добавить регион' and chat_id in admins_list:
user_state_manager.set_state(chat_id, "ADD_REGION")
handle_region_manager(chat_id, 'add')
elif text.lower() == 'удалить регион' and chat_id in admins_list:
user_state_manager.set_state(chat_id, "REMOVE_REGION")
handle_region_manager(chat_id, 'remove')
elif text.lower() == 'назад':
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, "Действие отменено.")
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)}")
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}")
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, "Вы не подписаны ни на один регион.")
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, "Действие отменено.")
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)}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_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
bot.clear_step_handler_by_chat_id(chat_id)
bot.send_message(chat_id,f"Действие отменено")
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
bot.clear_step_handler_by_chat_id(chat_id)
bot.send_message(chat_id,f"Действие отменено")
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 admin region management commands
######################################################################################################################
def handle_region_manager(chat_id: int, action: str):
if action == 'add':
bot.send_message(chat_id, "Введите ID и название региона в формате:\n ")
bot.register_next_step_handler_by_chat_id(chat_id, process_add_region)
elif action == 'remove':
bot.send_message(chat_id, "Введите ID региона, который хотите сделать неактивным")
bot.register_next_step_handler_by_chat_id(chat_id, process_remove_region)
######################################################################################################################
# Handle admin region management commands
######################################################################################################################
class RegionManager:
def __init__(self, db_path):
self.db_path = db_path
def add_region(self, region_id: int, region_name: str):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем наличие региона
cursor.execute('SELECT region_name, active FROM regions WHERE region_id = ?', (region_id,))
result = cursor.fetchone()
if result:
existing_region_name, active = result
if existing_region_name == region_name:
# Регион уже существует с этим именем
cursor.execute('UPDATE regions SET active = TRUE WHERE region_id = ?', (region_id,))
conn.commit()
return "activated", existing_region_name
else:
return "exists", existing_region_name
else:
# Добавляем новый регион
cursor.execute('INSERT OR IGNORE INTO regions (region_id, region_name) VALUES (?, ?)', (region_id, region_name))
conn.commit()
return "added", region_name
def remove_region(self, region_id: int):
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Проверяем, существует ли регион
cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,))
count = cursor.fetchone()[0]
if count == 0:
return False # Регион не найден
# Деактивируем регион и обновляем подписки
cursor.execute('UPDATE regions SET active = FALSE WHERE region_id = ?', (region_id,))
cursor.execute('UPDATE subscriptions SET active = FALSE WHERE region_id = ? AND active = TRUE', (region_id,))
conn.commit()
return True
def log_event(self, chat_id: int, username: str, action: str):
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
with db_lock, sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)'
cursor.execute(query, (chat_id, username, action, timestamp))
conn.commit()
region_manager = RegionManager(DB_PATH)
def process_add_region(message):
chat_id = message.chat.id
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
try:
parts = message.text.split()
if len(parts) < 2:
raise ValueError("Неверный формат")
region_id, region_name = parts[0], ' '.join(parts[1:])
status, existing_region_name = region_manager.add_region(region_id, region_name)
if status == "activated":
bot.send_message(chat_id, f"Регион {region_id} - {region_name} активирован.")
region_manager.log_event(chat_id, username, f"Admin reactivated region {region_id} - {region_name}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
elif status == "added":
bot.send_message(chat_id, f"Регион {region_id} - {region_name} добавлен.")
region_manager.log_event(chat_id, username, f"Admin added region {region_id} - {region_name}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
elif status == "exists":
markup = telebot.types.InlineKeyboardMarkup()
markup.add(telebot.types.InlineKeyboardButton(text="Заменить",
callback_data=f"replace_{region_id}_{urllib.parse.quote(region_name)}"))
markup.add(
telebot.types.InlineKeyboardButton(text="Активировать старый", callback_data=f"reactivate_{region_id}"))
markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_region_{chat_id}"))
bot.send_message(chat_id,
f"Регион {region_id} уже существует с названием {existing_region_name}. Хотите заменить его или активировать старый регион?",
reply_markup=markup)
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
except (IndexError, ValueError):
bot.send_message(chat_id, "Неверный формат. Используйте: ")
except Exception as e:
telebot.logger.error(f"Unexpected error: {e}")
bot.send_message(chat_id, "Произошла ошибка при обработке запроса.")
@bot.callback_query_handler(func=lambda call: call.data.startswith("replace_") or call.data.startswith(
"reactivate_") or call.data.startswith("cancel_region_"))
def handle_region_action(call):
parts = call.data.split("_", 2)
action = parts[0]
region_id = parts[1]
region_name = parts[2] if len(parts) > 2 else None
chat_id = call.message.chat.id
username = f"@{call.message.from_user.username}" if call.message.from_user.username else "N/A"
if action == "replace":
if region_name:
region_name = urllib.parse.unquote(region_name)
region_manager.add_region(region_id, region_name)
bot.send_message(chat_id, f"Регион {region_id} обновлен до {region_name} и активирован.")
region_manager.log_event(chat_id, username,
f"Admin replaced and reactivated region {region_id} - {region_name}")
telebot.logger.info(f"Admin {username} replaced and reactivated region {region_id} - {region_name}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
elif action == "reactivate":
region_manager.add_region(region_id, region_name)
bot.send_message(chat_id, f"Регион {region_id} активирован.")
region_manager.log_event(chat_id, username, f"Admin reactivated region {region_id} - {region_name}")
telebot.logger.info(f"Admin {username} activate {region_id} - {region_name}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
elif action == "cancel_region":
bot.send_message(chat_id, "Действие отменено.")
telebot.logger.info(f"Admin {username} canceled region actions.")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
bot.edit_message_reply_markup(chat_id=chat_id, message_id=call.message.message_id, reply_markup=None)
bot.answer_callback_query(call.id)
return show_settings_menu(chat_id)
def process_remove_region(message):
chat_id = message.chat.id
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
try:
region_id = message.text.split()[0]
success = region_manager.remove_region(region_id)
if success:
bot.send_message(chat_id, f"Регион {region_id} теперь неактивен, и все активные подписки обновлены.")
region_manager.log_event(chat_id, username, f"Admin {username} deactivated region {region_id}")
telebot.logger.info(f"Admin {username} deactivated region {region_id}")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
else:
bot.send_message(chat_id, f"Регион с ID {region_id} не существует.")
user_state_manager.set_state(chat_id, "SETTINGS_MENU")
show_settings_menu(chat_id)
except IndexError:
bot.send_message(chat_id, "Неверный формат. Используйте: ")
######################################################################################################################
##
##
######################################################################################################################
# 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 is_whitelisted(chat_id):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
return
user_regions = get_user_subscribed_regions(chat_id)
if not user_regions:
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 = format_regions_list(user_regions)
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 is_whitelisted(chat_id):
bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
return
regions = get_sorted_regions() # Используем функцию для получения отсортированных регионов
if not regions:
bot.send_message(chat_id, "Нет активных регионов.")
else:
regions_list = format_regions_list(regions)
bot.send_message(chat_id, f"Активные регионы:\n{regions_list}")
show_settings_menu(chat_id)
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():
# connection, channel = rabbitmq_connection()
#
# for method_frame, properties, body in channel.consume(RABBITMQ_QUEUE):
# 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)
# channel.basic_ack(method_frame.delivery_tag)
# except Exception as e:
# telebot.logger.error(f"Error sending message from queue: {e}")
# # Optionally, you can nack the message to requeue it
# # channel.basic_nack(method_frame.delivery_tag)
#
# connection.close()
async def consume_from_queue():
while True: # Бесконечный цикл для переподключения
try:
# Подключение к RabbitMQ
connection = await aio_pika.connect_robust(RABBITMQ_URL_FULL)
async with connection:
# Открываем канал
channel = await connection.channel()
# Объявляем очередь (если нужно)
queue = await channel.declare_queue(RABBITMQ_QUEUE, durable=True)
# Потребляем сообщения
async for message in queue:
async with message.process(): # Авто подтверждение сообщения
try:
# Парсинг сообщения
data = json.loads(message.body)
# Проверка структуры сообщения
if not isinstance(data, dict):
raise ValueError("Invalid message format: Expected a dictionary")
# Извлечение необходимых данных
chat_id = data.get("chat_id")
username = data.get("username")
message_text = data.get("message")
# Проверка обязательных полей
if not all([chat_id, username, message_text]):
raise ValueError(f"Missing required fields in message: {data}")
# Отправляем сообщение
await send_notification_message(chat_id, message_text, username)
except json.JSONDecodeError:
# Логируем некорректный JSON
telebot.logger.error(f"Failed to decode message: {message.body}")
except ValueError as ve:
# Логируем ошибку формата сообщения
telebot.logger.error(f"Invalid message: {ve}")
except Exception as e:
# Логируем общую ошибку при обработке
telebot.logger.error(f"Error sending message from queue: {e}")
except aio_pika.exceptions.AMQPError as e:
# Логируем ошибку RabbitMQ и переподключаемся
telebot.logger.error(f"RabbitMQ error: {e}")
except Exception as e:
# Логируем общую ошибку и ждем перед переподключением
telebot.logger.error(f"Critical error in consume_from_queue: {e}")
finally:
# Задержка перед переподключением
await asyncio.sleep(5)
async def send_message(chat_id, message, is_notification=False):
try:
if is_notification:
await rate_limit_semaphore.acquire()
parse_mode = 'HTML'
# Используем partial для передачи именованных аргументов в bot.send_message
func_with_args = partial(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)
# formatted_message = message.replace('\n', ' ').replace('\r', '')
# telebot.logger.info(f'Send notification to {username} {chat_id} from RabbitMQ [{formatted_message}]')
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}")
def extract_region_number(host):
# Используем регулярное выражение для извлечения цифр после первого символа и до первой буквы
match = re.match(r'^.\d+', host)
if match:
return match.group(0)[1:] # Возвращаем строку без первого символа
return None
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 is_whitelisted(chat_id):
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"))
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}.")
# Убираем клавиатуру
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 "Все события"
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}")
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 = get_sorted_regions() # Используем функцию get_regions для получения регионов
start_index = 0
markup = create_region_keyboard(regions, start_index)
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 = 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:
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}"))
bot.send_message(chat_id, "Выберите группу хостов или получите события по всем группам региона:", reply_markup=markup)
except Exception as e:
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 get_triggers_for_group(chat_id, group_id):
triggers = get_zabbix_triggers(group_id) # Получаем все активные события без периода
if not triggers:
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:
bot.send_message(chat_id, f"Нет активных событий.")
show_main_menu(chat_id)
except Exception as e:
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:
bot.send_message(chat_id, trigger, parse_mode="html")
time.sleep(1 / 5)
def escape_telegram_chars(text):
"""
Экранирует запрещённые символы для Telegram API:
< -> <
> -> >
& -> &
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
"""
replacements = {
'&': '&',
'<': '<',
'>': '>',
'"': '"', # Для кавычек
}
# Применяем замены
for char, replacement in replacements.items():
text = text.replace(char, replacement)
return text
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"{priority} Host: {host}\n"
f"IP: {host_ip}\n"
f"Описание: {description}\n"
f"Критичность: {severity}\n"
f"Время создания: {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
@app.route(BASE_URL + '/webhook', methods=['POST'])
def webhook():
try:
# Получаем данные и логируем
data = request.get_json()
app.logger.info(f"Получены данные: {data}")
# Генерация хеша события и логирование
event_hash = 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:
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
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"Описание: {msg}\n"
f"Критичность: {data['severity']}\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
else:
message = (
f"✅ {data['host']} ({data['ip']})\n"
f"Описание: {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}")
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
# Маршрут для добавления пользователя
@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}")
bot.send_message(chat_id, "Регистрация пройдена успешно.")
# DEBUG: Попытка добавления пользователя в whitelist
app.logger.debug(f"Добавление пользователя {telegram_id} в whitelist")
success = rundeck_add_to_whitelist(chat_id, telegram_id, user_email)
if success:
# INFO: Пользователь успешно добавлен в whitelist
app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.")
user_state_manager.set_state(chat_id, "MAIN_MENU")
# DEBUG: Показ основного меню пользователю
app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}")
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()
# DEBUG: Формирование словаря пользователей
app.logger.debug("Формирование словаря пользователей")
users_dict = {id: {'id': id, 'username': username, 'email': email, 'events': [], 'worker': '', 'subscriptions': []}
for 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
# Маршрут для отображения HTML-страницы с информацией о пользователях
@app.route(BASE_URL + '/users', methods=['GET'])
def view_users():
return render_template('users.html')
# Маршрут для добавления региона
@app.route(BASE_URL + '/regions/add', methods=['POST'])
def add_region():
data = request.json
region_id: int = data.get('region_id')
region_name: str = data.get('region_name')
if not region_id or not region_name:
return jsonify({"status": "error", "message": "Invalid input"}), 400
result = region_api.add_region(region_id, region_name)
return jsonify(result)
# Маршрут для удаления региона
@app.route(BASE_URL + '/regions/del', methods=['POST'])
def del_region():
data = request.json
region_id = data.get('region_id')
if not region_id:
return jsonify({"status": "error", "message": "Invalid input"}), 400
result = region_api.remove_region(region_id)
return jsonify(result)
# Маршрут для получения списка регионов
@app.route(BASE_URL + '/regions/get', methods=['GET'])
def get_regions():
regions = region_api.get_regions()
return jsonify(regions)
@app.route(BASE_URL + '/regions/edit', methods=['POST'])
def edit_region():
data = request.json
region_id = data.get('region_id')
active = data.get('active')
# Проверка валидности данных
if not region_id and active:
return jsonify({"status": "error", "message": "Invalid data received"}), 400
# Обновление региона
result = region_api.update_region_status(region_id, active)
return jsonify(result)
# Маршрут для рендеринга страницы управления регионами
@app.route(BASE_URL + '/regions', methods=['GET'])
def regions_page():
return render_template('regions.html')
# Управление уровнями логирования для Flask
@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
# Управление уровнями логирования для Telebot
@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
# Test functions for admin
def simulate_event(message):
chat_id = message.chat.id
test_event = {
"host": "12",
"msg": "Тестовое сообщение",
"date_reception": "12757175125",
"severity": "5",
"tags": "OTC,OFS,NFS",
"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)
app.logger.info(f"Response from webhook: {response.status_code} - {response.text}")
bot.send_message(chat_id, f"Тестовое событие отправлено. Статус ответа: {response.status_code}")
def simulate_triggers(message):
chat_id = message.chat.id
regions = ["12", "19", "35", "40"]
trigger_messages = []
for region_id in regions:
triggers = get_zabbix_triggers(region_id)
if triggers:
trigger_messages.append(f"Регион {region_id}:\n{triggers}")
if trigger_messages:
bot.send_message(chat_id, "\n\n".join(trigger_messages), parse_mode="html")
else:
bot.send_message(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():
# Инициализация базы данных
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()