Telezab/telezab.py
Влад Зверев dd66cb5712 rework Active triggers function
rework settings menu button
add function to choose what severity level you want to receive
2024-09-09 17:05:46 +05:00

1587 lines
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

import os
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
from threading import Thread, Lock, Timer
import sqlite3
import time
import asyncio
import aiohttp
import pika
import json
from concurrent.futures import ThreadPoolExecutor
from pyzabbix import ZabbixAPI
import requests
from pytz import timezone
from datetime import datetime, timedelta
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)
self.setStream(stream)
def setStream(self, stream):
super().setStream(stream)
if hasattr(stream, 'reconfigure'):
stream.reconfigure(encoding='utf-8')
# Определение пути к основному лог-файлу
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():
# Получаем дату предыдущего дня
yesterday_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')
# Проверяем существует ли основной лог-файл
if os.path.exists(LOG_FILE):
# Путь к архиву в той же папке
archive_name = f"app_{yesterday_date}.zip"
archive_path = os.path.join(os.path.dirname(LOG_FILE), archive_name)
# Создание архива и добавление лог-файла
with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.write(LOG_FILE, arcname=os.path.basename(LOG_FILE))
# Удаление старого лог-файла после архивирования
os.remove(LOG_FILE)
class FilterByMessage(logging.Filter):
def filter(self, record):
# Фильтруем сообщения, содержащие 'Received 1 new updates'
return 'Received ' not in record.getMessage()
# Initialize Flask application
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',
'stream': 'ext://sys.stdout',
'formatter': 'default',
'filters': ['filter_by_message']
},
'file': {
'class': 'logging.FileHandler',
'filename': 'logs/app.log', # Указываем путь к общим логам
'formatter': 'default',
'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': {
'filter_by_message': {
'()': FilterByMessage,
},
},
'loggers': {
'flask': {
'level': 'WARNING',
'handlers': ['file', 'error_file'], # Записываем логи во все файлы
'propagate': False,
},
'telebot': {
'level': 'WARNING',
'handlers': ['file', 'error_file'], # Логи Telebot
'propagate': False,
},
},
'root': {
'level': 'WARNING',
'handlers': ['file', 'error_file'], # Корневой логгер пишет в файлы
}
})
# Настройка уровня логирования для Flask
app.logger.setLevel(logging.DEBUG)
# Настройка pyTelegramBotAPI logger
telebot.logger = logging.getLogger('telebot')
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
# Define states
NOTIFICATION_MODE = 1
SETTINGS_MODE = 2
# Dictionary to keep track of user states and timers
user_states = {}
user_timers = {}
def init_db():
global st
st = datetime.now()
try:
with db_lock:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create events table
cursor.execute('''CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT UNIQUE,
data TEXT,
delivered BOOLEAN)''')
# Create subscriptions table with username and active flag
cursor.execute('''CREATE TABLE IF NOT EXISTS 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))''')
# Create whitelist table
cursor.execute('''CREATE TABLE IF NOT EXISTS whitelist (
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,
region_name TEXT,
active BOOLEAN DEFAULT TRUE)''')
# Create user events table for logging
cursor.execute('''CREATE TABLE IF NOT EXISTS user_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER,
username TEXT,
action TEXT,
timestamp TEXT)''')
# Insert sample regions
cursor.execute('''INSERT OR IGNORE INTO regions (region_id, region_name) VALUES
('01', 'Адыгея'),
('02', 'Башкортостан (Уфа)'),
('04', 'Алтай'),
('19', 'Республика Хакасия')''')
conn.commit()
app.logger.info("Database initialized successfully.")
except Exception as e:
app.logger.error(f"Error initializing database: {e}")
finally:
conn.close()
app.logger.info(f"init_db completed in {datetime.now() - st}")
# 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.debug(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()
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()
# 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.debug(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
# Get list of regions
def get_regions():
with db_lock:
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()
conn.close()
return regions
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()
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()
# Handle state transitions
def set_user_state(chat_id, state):
user_states[chat_id] = state
if state == SETTINGS_MODE:
start_settings_timer(chat_id)
elif state == NOTIFICATION_MODE:
cancel_settings_timer(chat_id)
def start_settings_timer(chat_id):
if chat_id in user_timers:
user_timers[chat_id].cancel()
timer = Timer(300, transition_to_notification_mode, [chat_id])
user_timers[chat_id] = timer
timer.start()
def cancel_settings_timer(chat_id):
if chat_id in user_timers:
user_timers[chat_id].cancel()
del user_timers[chat_id]
def reset_settings_timer(chat_id):
if chat_id in user_timers:
user_timers[chat_id].cancel()
start_settings_timer(chat_id)
def transition_to_notification_mode(chat_id):
set_user_state(chat_id, NOTIFICATION_MODE)
bot.send_message(chat_id, "Вы были автоматически переведены в режим получения уведомлений.")
show_main_menu(chat_id)
telebot.logger.info(f"User {chat_id} automatically transitioned to notification mode.")
# Main menu for users
def show_main_menu(chat_id):
markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True)
if is_whitelisted(chat_id):
markup.add('Настройки', 'Помощь', 'Активные триггеры')
else:
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 chat_id in admins_list:
markup.row('Добавить регион', 'Удалить регион')
markup.row('Назад')
return markup
# Settings menu for users
def show_settings_menu(chat_id):
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 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)
else:
if text == 'регистрация':
handle_register(message)
elif text == 'настройки':
set_user_state(chat_id, SETTINGS_MODE)
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)
# Handle settings menu button presses
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 == 'отписаться':
handle_unsubscribe(message)
elif text == 'мои подписки':
handle_my_subscriptions(message)
elif text == 'активные регионы':
handle_active_regions(message)
elif text == 'режим уведомлений':
handle_notification_mode(message)
elif text == 'добавить регион' and chat_id in admins_list:
prompt_admin_for_region(chat_id, 'add')
elif text == 'удалить регион' and chat_id in admins_list:
prompt_admin_for_region(chat_id, 'remove')
elif text == 'тестовое событие' and chat_id in admins_list:
simulate_event(message)
elif text == 'тест триггеров' and chat_id in admins_list:
simulate_triggers(message)
elif text == 'назад':
set_user_state(chat_id, NOTIFICATION_MODE)
show_main_menu(chat_id)
else:
bot.send_message(chat_id, "Команда не распознана.")
show_settings_menu(chat_id)
# Handle /subscribe command to subscribe to a region
@bot.message_handler(commands=['subscribe', 'sub'])
def handle_subscribe(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}")
return
username = message.from_user.username
if username:
username = f"@{username}"
else:
username = "N/A"
regions_list = format_regions_list(get_sorted_regions())
bot.send_message(chat_id, f"Отправьте номер или номера регионов, на которые хотите подписаться (через запятую):\n{regions_list}\n\nНапишите 'отмена' для отмены.")
bot.register_next_step_handler_by_chat_id(chat_id, process_subscription, chat_id, username)
def process_subscription(message, chat_id, username):
if message.text.lower() == 'отмена':
bot.send_message(chat_id, "Действие отменено.")
return show_settings_menu(chat_id)
region_ids = message.text.split(',')
valid_region_ids = get_regions()
valid_region_ids = [region[0] for region in valid_region_ids]
with db_lock:
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} не существует или недопустимый формат. Введите только существующие номера регионов.")
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}")
cursor.execute(query, (chat_id, region_id, username))
if cursor.rowcount == 0:
query = 'UPDATE subscriptions SET active = TRUE WHERE chat_id = ? AND region_id = ?'
telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, region_id={region_id}")
cursor.execute(query, (chat_id, region_id))
conn.commit()
conn.close()
bot.send_message(chat_id, f"Подписка на регионы: {', '.join(region_ids)} оформлена.")
telebot.logger.info(f"User {chat_id} ({username}) subscribed to regions: {', '.join(region_ids)}.")
log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(region_ids)}")
show_settings_menu(chat_id)
# Handle /unsubscribe command to unsubscribe from a region
@bot.message_handler(commands=['unsubscribe'])
def handle_unsubscribe(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}")
return
user_regions = get_user_subscribed_regions(chat_id)
if not user_regions:
bot.send_message(chat_id, "Вы не подписаны ни на один регион.")
return show_settings_menu(chat_id)
else:
regions_list = format_regions_list(user_regions)
bot.send_message(chat_id, f"Отправьте номер или номера регионов, от которых хотите отписаться (через запятую):\n{regions_list}\n\nНапишите 'отмена' для отмены.")
bot.register_next_step_handler_by_chat_id(chat_id, process_unsubscription, chat_id)
# Пример функции, которая использует bot и log
def process_unsubscription(message, chat_id):
if message.text.lower() == 'отмена':
bot.send_message(chat_id, "Действие отменено.")
return show_settings_menu(chat_id)
region_ids = message.text.split(',')
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(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} не существует или недопустимый формат. Введите только номера регионов, на которые вы подписаны.")
return show_settings_menu(chat_id)
query = 'UPDATE subscriptions SET active = FALSE WHERE chat_id = ? AND region_id = ?'
telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}, region_id={region_id}")
cursor.execute(query, (chat_id, region_id))
conn.commit()
conn.close()
bot.send_message(chat_id, f"Отписка от регионов: {', '.join(region_ids)} оформлена.")
telebot.logger.info(f"User {chat_id} unsubscribed from regions: {', '.join(region_ids)}.")
username = "@" + message.from_user.username if message.from_user.username else "N/A"
log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(region_ids)}")
show_settings_menu(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 = (
'<b>/start</b> - Показать меню бота\n'
'<b>Настройки</b> - Перейти в режим настроек и управления подписками\n'
'<b>Активные тригеры</b> - Получение активных проблем за последние 24 часа\n'
'<b>Помощь</b> - <a href="https://confluence.is-mis.ru/pages/viewpage.action?pageId=460596141">Описание всех возможностей бота</a>'
)
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"
bot.send_message(chat_id,
f"Ваш chat ID: {chat_id}\nВаше имя пользователя: {username}\nДля продолжения регистрации необходимо отправить письмо на почту {SUPPORT_EMAIL}\nВ теме письма указать Telegram Bot\nВ теле письма указать полученные данные")
log_user_event(chat_id, username, "Requested registration")
# Handle admin region management commands
def prompt_admin_for_region(chat_id, action):
if action == 'add':
bot.send_message(chat_id, "Введите ID и название региона в формате: <region_id> <region_name>")
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)
def process_add_region(message):
chat_id = message.chat.id
try:
region_id, region_name = message.text.split()[0], ' '.join(message.text.split()[1:])
with db_lock:
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}")
cursor.execute(query, (region_id,))
result = cursor.fetchone()
if result:
existing_region_name, active = result
if existing_region_name == region_name:
query = 'UPDATE regions SET active = TRUE WHERE region_id = ?'
telebot.logger.debug(f"Executing query: {query} with region_id={region_id}")
cursor.execute(query, (region_id,))
bot.send_message(chat_id, f"Регион {region_id} - {region_name} активирован.")
telebot.logger.info(f"Admin {chat_id} reactivated region {region_id} - {region_name}.")
else:
markup = telebot.types.InlineKeyboardMarkup()
markup.add(telebot.types.InlineKeyboardButton(text="Заменить", callback_data=f"replace_{region_id}_{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)
else:
query = 'INSERT OR IGNORE INTO regions (region_id, region_name) VALUES (?, ?)'
telebot.logger.debug(f"Executing query: {query} with region_id={region_id}, region_name={region_name}")
cursor.execute(query, (region_id, region_name))
bot.send_message(chat_id, f"Регион {region_id} - {region_name} добавлен.")
telebot.logger.info(f"Admin {chat_id} added region {region_id} - {region_name}.")
conn.commit()
conn.close()
except (IndexError, ValueError):
bot.send_message(chat_id, "Неверный формат. Используйте: <region_id> <region_name>")
@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
if not region_name:
bot.send_message(chat_id, "Ошибка: Недостаточно данных для выполнения действия.")
bot.answer_callback_query(call.id) # Завершение обработки callback
bot.delete_message(chat_id, call.message.message_id)
return show_settings_menu(chat_id)
with db_lock:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
if action == "replace":
query = 'UPDATE regions SET region_name = ?, active = TRUE WHERE region_id = ?'
telebot.logger.debug(f"Executing query: {query} with region_name={region_name}, region_id={region_id}")
cursor.execute(query, (region_name, region_id))
bot.send_message(chat_id, f"Регион {region_id} обновлен до {region_name} и активирован.")
telebot.logger.info(f"Admin {chat_id} replaced and reactivated region {region_id} with {region_name}.")
elif action == "reactivate":
query = 'UPDATE regions SET active = TRUE WHERE region_id = ?'
telebot.logger.debug(f"Executing query: {query} with region_id={region_id}")
cursor.execute(query, (region_id,))
bot.send_message(chat_id, f"Регион {region_id} активирован.")
telebot.logger.info(f"Admin {chat_id} reactivated region {region_id}.")
elif action == "cancel_region":
bot.send_message(chat_id, "Действие отменено.")
telebot.logger.info(f"Admin {chat_id} canceled region action.")
conn.commit()
conn.close()
bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None)
show_settings_menu(chat_id)
bot.answer_callback_query(call.id) # Завершение обработки callback
def process_remove_region(message):
chat_id = message.chat.id
try:
region_id = message.text.split()[0]
with db_lock:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Проверка существования региона
query = 'SELECT COUNT(*) FROM regions WHERE region_id = ?'
cursor.execute(query, (region_id,))
count = cursor.fetchone()[0]
if count == 0:
bot.send_message(chat_id, f"Регион с ID {region_id} не существует.")
return show_settings_menu(chat_id)
query = 'UPDATE regions SET active = FALSE WHERE region_id = ?'
telebot.logger.debug(f"Executing query: {query} with region_id={region_id}")
cursor.execute(query, (region_id,))
query = 'UPDATE subscriptions SET active = FALSE WHERE region_id = ? AND active = TRUE'
telebot.logger.debug(f"Executing query: {query} with region_id={region_id}")
cursor.execute(query, (region_id,))
conn.commit()
conn.close()
bot.send_message(chat_id, f"Регион {region_id} теперь неактивен и все активные подписки обновлены.")
telebot.logger.info(f"Admin {chat_id} set region {region_id} to inactive and updated subscriptions.")
except IndexError:
bot.send_message(chat_id, "Неверный формат. Используйте: <region_id>")
show_settings_menu(chat_id)
# Handle displaying active subscriptions for a user
def handle_my_subscriptions(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}")
return
user_regions = get_user_subscribed_regions(chat_id)
if not user_regions:
bot.send_message(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}")
show_settings_menu(chat_id)
# Handle displaying all active regions
def handle_active_regions(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}")
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)
# RabbitMQ configuration
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'localhost')
RABBITMQ_QUEUE = 'telegram_notifications'
def rabbitmq_connection():
# Создаем объект учетных данных
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
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']
message_text = message['message']
try:
await send_notification_message(chat_id, message_text)
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 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)
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"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)
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
@app.route('/webhook', methods=['POST'])
def webhook():
try:
data = request.get_json()
app.logger.info(f"Received data: {data}")
event_hash = hash_data(data)
with db_lock:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM events')
count = cursor.fetchone()[0]
if count >= 200:
query = 'DELETE FROM events WHERE id = (SELECT MIN(id) FROM events)'
app.logger.debug(f"Executing query: {query}")
cursor.execute(query)
# Извлечение номера региона из поля host
region_id = extract_region_number(data.get("host"))
if region_id is None:
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 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,))
region_row = cursor.fetchone()
if region_row and region_row[0]: # Check if region_row is not None and if active
message = format_message(data)
undelivered = False
for chat_id, username in results:
app.logger.debug(f"Queueing message: {message} to chat_id={chat_id}, username={username}")
try:
send_to_queue({'chat_id': chat_id, 'message': message})
app.logger.info(f"Queued alert for {chat_id} ({username}) for region {region_id}")
except Exception as e:
app.logger.error(f"Failed to send message to {chat_id} ({username}): {e}")
undelivered = True
if undelivered:
query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)'
app.logger.debug(f"Executing query: {query} with hash={event_hash}, data={data}, delivered={False}")
cursor.execute(query, (event_hash, str(data), False))
conn.commit()
conn.close()
return jsonify({"status": "success"}), 200
except sqlite3.OperationalError as e:
app.logger.error(f"Database operation error: {e}")
return jsonify({"status": "error", "message": "Database operation error"}), 500
except ValueError as e:
app.logger.error(f"Value error: {e}")
return jsonify({"status": "error", "message": "Invalid data"}), 400
except Exception as e:
app.logger.error(f"Unexpected error: {e}")
return jsonify({"status": "error", "message": "Internal server error"}), 500
def format_message(data):
try:
status_emoji = "⚠️" if data['status'].upper() == "PROBLEM" else ""
priority_map = {
'4': 'Высокая',
'5': 'Авария'
}
priority = priority_map.get(data['severity'], 'Неизвестно')
if data['status'].upper() == "PROBLEM":
message = (
f"⚠️ {data['host']} ({data['ip']})\n"
f"{data['msg']}\n"
f"Критичность: {data['severity']}"
)
if 'link' in data:
message += f'\nURL: <a href="{data['link']}">Ссылка на график</a>'
return message
else:
message = (
f"{data['host']} ({data['ip']})\n"
f"<b>Описание</b>: {data['msg']}\n"
f"<b>Критичность</b>: {data['severity']}\n"
f"<b>Проблема устранена!</b>\n"
f"<b>Время устранения проблемы</b>: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(data['date_reception'])))}\n"
)
if 'link' in data:
message += f'<b>URL</b>: <a href="{data['link']}">Ссылка на график</a>'
return message
except KeyError as e:
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 and user_email:
rundeck_add_to_whitelist(chat_id, telegram_id, user_email)
app.logger.info(f"User {telegram_id} added to whitelist.")
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
@app.route('/debug/flask', methods=['POST'])
def toggle_flask_debug():
try:
data = request.get_json()
level = data.get('level', 'DEBUG').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('/debug/telebot', methods=['POST'])
def toggle_telebot_debug():
try:
data = request.get_json()
level = data.get('level', 'DEBUG').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
# Фаза 1: Запрос активных триггеров и выбор региона с постраничным переключением
def handle_active_triggers(message):
chat_id = message.chat.id
regions = get_user_subscribed_regions(chat_id) # Функция для получения доступных регионов
if not regions:
bot.send_message(chat_id, "У вас нет подписанных регионов.")
return
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=5):
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_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)
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
# Убираем клавиатуру, если регион был выбран
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:
# Получаем группы хостов для выбранного региона
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()]
# Если нет групп
if not filtered_groups:
bot.send_message(chat_id, "Нет групп хостов для этого региона.")
return_to_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="Все группы региона", callback_data=f"all_groups_{region_id}"))
bot.send_message(chat_id, "Выберите группу хостов или получите триггеры по всем группам региона:", reply_markup=markup)
except Exception as e:
bot.send_message(chat_id, "Ошибка при подключении к Zabbix API.")
return_to_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) # Сразу получаем триггеры для группы
# Если выбраны все группы региона
elif call.data.startswith("all_groups_"):
region_id = call.data.split("_")[2]
get_triggers_for_all_groups(chat_id, region_id) # Сразу получаем триггеры для всех групп региона
# # Фаза 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:
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"Нет активных триггеров.")
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:
< -> &lt;
> -> &gt;
& -> &amp;
Также проверяет на наличие запрещённых HTML-тегов и другие проблемы с форматированием.
"""
replacements = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;', # Для кавычек
}
# Применяем замены
for char, replacement in replacements.items():
text = text.replace(char, replacement)
return text
def get_zabbix_triggers(group_id):
try:
zapi = ZabbixAPI(ZABBIX_URL)
zapi.login(api_token=ZABBIX_API_TOKEN)
# Рассчитываем временной диапазон на основе переданного количества часов
# 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"}, # Высокий приоритет и авария
only_true=1,
active=1,
withLastEventUnacknowledged=1,
# 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')
priority_map = {
'4': 'Высокая',
'5': 'Авария'
}
trigger_messages = []
for trigger in triggers:
event_time_epoch = int(trigger['lastEvent']['clock'])
event_time = datetime.fromtimestamp(event_time_epoch, tz=moskva_tz)
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']]
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)
# Заменяем {ITEM.LASTVALUE1} и другие на соответствующие значения
for i, item in enumerate(trigger['items']):
lastvalue_placeholder = f"{{ITEM.LASTVALUE{i + 1}}}"
if lastvalue_placeholder in description:
description = description.replace(lastvalue_placeholder, item['lastvalue'])
event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск')
message = (f"<b>Host</b>: {host}\n"
f"<b>Критичность</b>: {priority}\n"
f"<b>Описание</b>: {description}\n"
f"<b>Время создания</b>: {event_time_formatted}\n"
# f"ItemIDs: {', '.join(item_ids)}\n"
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>')
trigger_messages.append(message)
return trigger_messages
except Exception as e:
telebot.logger.error(f"Error fetching triggers for group {group_id}: {e}")
return None
# 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, "Нет активных проблем по указанным регионам за последние 24 часа.")
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 schedule_jobs():
schedule.every().day.at("00:00").do(archive_old_logs)
while True:
schedule.run_pending()
time.sleep(60) # Проверять раз в минуту
# Основная функция для запуска
def main():
# Инициализация базы данных
init_db()
# Запуск Flask и бота в отдельных потоках
Thread(target=run_flask, daemon=True).start()
Thread(target=run_polling, daemon=True).start()
# Запуск планировщика задач в отдельном потоке
Thread(target=schedule_jobs, daemon=True).start()
# Запуск асинхронных задач
asyncio.run(consume_from_queue())
if __name__ == '__main__':
main()