from flask import Flask, request, jsonify import telebot from dotenv import load_dotenv import os import hashlib import logging from logging.config import dictConfig from threading import Thread, Lock, Timer import sqlite3 import time # Load environment variables load_dotenv() # Configure logging DEBUG_LOGGING = os.getenv('DEBUG_LOGGING', 'false').lower() == 'true' if DEBUG_LOGGING: log_level = 'DEBUG' else: log_level = 'INFO' dictConfig({ 'version': 1, 'formatters': {'default': { 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', }}, 'handlers': {'wsgi': { 'class': 'logging.StreamHandler', 'stream': 'ext://flask.logging.wsgi_errors_stream', 'formatter': 'default' }}, 'root': { 'level': log_level, 'handlers': ['wsgi'] } }) app = Flask(__name__) # Get the token from environment variables TOKEN = os.getenv('TELEGRAM_TOKEN') if not TOKEN: raise ValueError("No TELEGRAM_TOKEN found in environment variables") ADMIN_CHAT_IDS = os.getenv('ADMIN_CHAT_IDS', '').split(',') bot = telebot.TeleBot(TOKEN) # Lock for database operations db_lock = Lock() # Define states NOTIFICATION_MODE = 1 SETTINGS_MODE = 2 # Dictionary to keep track of user states and timers user_states = {} user_timers = {} # Initialize SQLite database def init_db(): with db_lock: conn = sqlite3.connect('telezab.db') 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, UNIQUE(chat_id, region_id))''') # Create whitelist table cursor.execute('''CREATE TABLE IF NOT EXISTS whitelist ( chat_id INTEGER PRIMARY KEY)''') # 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() 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('telezab.db') cursor = conn.cursor() query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' app.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): with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() query = 'INSERT OR IGNORE INTO whitelist (chat_id) VALUES (?)' app.logger.debug(f"Executing query: {query} with chat_id={chat_id}") cursor.execute(query, (chat_id,)) conn.commit() conn.close() # Remove user from whitelist def remove_from_whitelist(chat_id): with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() query = 'DELETE FROM whitelist WHERE chat_id = ?' app.logger.debug(f"Executing query: {query} with chat_id={chat_id}") cursor.execute(query, (chat_id,)) conn.commit() conn.close() # Get list of regions def get_regions(): with db_lock: conn = sqlite3.connect('telezab.db') 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 # Get list of regions a user is subscribed to def get_user_subscribed_regions(chat_id): with db_lock: conn = sqlite3.connect('telezab.db') 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 # Format regions list def format_regions_list(regions): return '\n'.join([f"{region_id} - {region_name}" for region_id, region_name in regions]) # Log user events def log_user_event(chat_id, username, action): timestamp = time.strftime('%Y-%m-%d %H:%M:%S') with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)' app.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() 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(30, 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 transition_to_notification_mode(chat_id): set_user_state(chat_id, 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) # Settings menu for users def show_settings_menu(chat_id): if user_states != 'NOTIFICATION_MODE': markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) if str(chat_id) in ADMIN_CHAT_IDS: markup.add('Подписаться', 'Отписаться', 'Мои подписки', 'Активные регионы', 'Добавить регион', 'Удалить регион', 'Назад') else: markup.add('Подписаться', 'Отписаться', 'Мои подписки', 'Активные регионы', 'Назад') bot.send_message(chat_id, "Вы находитесь в режиме настроек. Выберите действие:", reply_markup=markup) else: pass # Handle /start command @bot.message_handler(commands=['start']) def handle_start(message): chat_id = message.chat.id username = message.from_user.username if username: username = f"@{username}" else: username = "N/A" set_user_state(chat_id, NOTIFICATION_MODE) show_main_menu(chat_id) app.logger.info(f"User {chat_id} ({username}) started with command /start.") # Handle menu button presses @bot.message_handler(func=lambda message: True) def handle_menu_selection(message): chat_id = message.chat.id text = message.text.strip().lower() if user_states.get(chat_id, NOTIFICATION_MODE) == SETTINGS_MODE: 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) 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 text == 'подписаться': handle_subscribe(message) elif text == 'отписаться': handle_unsubscribe(message) elif text == 'мои подписки': handle_my_subscriptions(message) elif text == 'активные регионы': handle_active_regions(message) elif text == 'добавить регион' and str(chat_id) in ADMIN_CHAT_IDS: prompt_admin_for_region(chat_id, 'add') elif text == 'удалить регион' and str(chat_id) in ADMIN_CHAT_IDS: prompt_admin_for_region(chat_id, 'remove') 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']) def handle_subscribe(message): chat_id = message.chat.id if not is_whitelisted(chat_id): bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") app.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_regions()) bot.send_message(chat_id, f"Отправьте номер или номера регионов, на которые хотите подписаться (через запятую):\n{regions_list}\n\nНапишите 'отмена' для отмены.") bot.register_next_step_handler(message, process_subscription, chat_id, username) def process_subscription(message, chat_id, username): if message.text.lower() == 'отмена': bot.send_message(chat_id, "Действие отменено.") show_settings_menu(chat_id) return region_ids = message.text.split(',') with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() for region_id in region_ids: region_id = region_id.strip() if not region_id.isdigit(): bot.send_message(chat_id, "Недопустимый формат. Введите только номера регионов.") show_settings_menu(chat_id) return query = 'INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username, active) VALUES (?, ?, ?, TRUE)' app.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 = ?' app.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)} оформлена.") app.logger.info(f"User {chat_id} (@{username}) subscribed to regions: {', '.join(region_ids)}.") username = message.from_user.username if username: username = f"@{username}" else: username = "N/A" 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, "Вы не авторизованы для использования этого бота.") app.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: regions_list = format_regions_list(user_regions) bot.send_message(chat_id, f"Отправьте номер или номера регионов, от которых хотите отписаться (через запятую):\n{regions_list}\n\nНапишите 'отмена' для отмены.") bot.register_next_step_handler(message, process_unsubscription, chat_id) def process_unsubscription(message, chat_id): if message.text.lower() == 'отмена': bot.send_message(chat_id, "Действие отменено.") show_settings_menu(chat_id) return region_ids = message.text.split(',') with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() for region_id in region_ids: region_id = region_id.strip() if not region_id.isdigit(): bot.send_message(chat_id, "Недопустимый формат. Введите только номера регионов.") show_settings_menu(chat_id) return # Проверяем, существует ли указанный region_id в базе данных query_check = 'SELECT COUNT(*) FROM regions WHERE region_id = ?' cursor.execute(query_check, (region_id,)) result = cursor.fetchone() if not result or result[0] == 0: bot.send_message(chat_id, f"Регион с номером {region_id} не существует.") show_settings_menu(chat_id) return query = 'UPDATE subscriptions SET active = FALSE WHERE chat_id = ? AND region_id = ?' app.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)} оформлена.") app.logger.info(f"User {chat_id} unsubscribed from regions: {', '.join(region_ids)}.") username = message.from_user.username if username: username = f"@{username}" else: username = "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): help_text = ( "/start - Показать меню бота\n" "Настройки - Перейти в режим настроек и управлять подписками\n" "Помощь - Показать это сообщение" ) bot.send_message(message.chat.id, help_text) show_main_menu(message.chat.id) # Handle /register command for new user registration @bot.message_handler(commands=['register']) def handle_register(message): chat_id = message.chat.id username = message.from_user.username if username: username = f"@{username}" else: username = "N/A" markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) markup.add('Подтвердить регистрацию', 'Отмена') bot.send_message(chat_id, f"Ваш chat ID: {chat_id}, ваше имя пользователя: {username}. Запрос на одобрение отправлен администратору.", reply_markup=markup) log_user_event(chat_id, username, "Requested registration") bot.register_next_step_handler(message, process_register, chat_id, username) def process_register(message, chat_id, username): if message.text.lower() == 'отмена': bot.send_message(chat_id, "Регистрация отменена.") show_main_menu(chat_id) return if message.text.lower() == 'подтвердить регистрацию': for admin_chat_id in ADMIN_CHAT_IDS: markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) markup.add(f'/add_whitelist {chat_id}', 'Отмена') bot.send_message( admin_chat_id, f"Пользователь {username} ({chat_id}) запрашивает регистрацию.\n" f"Вы подтверждаете это действие?", reply_markup=markup ) bot.send_message(chat_id, "Запрос отправлен администратору для одобрения.") app.logger.info(f"User {chat_id} ({username}) requested registration.") else: bot.send_message(chat_id, "Некорректный выбор. Регистрация отменена.") show_main_menu(chat_id) # Handle admin region management commands def prompt_admin_for_region(chat_id, action): if action == 'add': bot.send_message(chat_id, "Введите ID и название региона в формате: ") 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('telezab.db') cursor = conn.cursor() query = 'SELECT region_name, active FROM regions WHERE region_id = ?' app.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 = ?' app.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} активирован.") app.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}")) 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 (?, ?)' app.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} добавлен.") app.logger.info(f"Admin {chat_id} added region {region_id} - {region_name}.") conn.commit() conn.close() except (IndexError, ValueError): bot.send_message(chat_id, "Неверный формат. Используйте: ") show_settings_menu(chat_id) @bot.callback_query_handler(func=lambda call: call.data.startswith("replace_") or call.data.startswith("reactivate_")) def handle_region_action(call): action, region_id, region_name = call.data.split("_", 2) chat_id = call.message.chat.id with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() if action == "replace": query = 'UPDATE regions SET region_name = ?, active = TRUE WHERE region_id = ?' app.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} и активирован.") app.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 = ?' app.logger.debug(f"Executing query: {query} with region_id={region_id}") cursor.execute(query, (region_id,)) bot.send_message(chat_id, f"Регион {region_id} активирован.") app.logger.info(f"Admin {chat_id} reactivated region {region_id}.") conn.commit() conn.close() show_settings_menu(chat_id) def process_remove_region(message): chat_id = message.chat.id try: region_id = message.text.split()[0] with db_lock: conn = sqlite3.connect('telezab.db') cursor = conn.cursor() query = 'UPDATE regions SET active = FALSE WHERE region_id = ?' app.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' app.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} теперь неактивен и все активные подписки обновлены.") app.logger.info(f"Admin {chat_id} set region {region_id} to inactive and updated subscriptions.") except IndexError: bot.send_message(chat_id, "Неверный формат. Используйте: ") show_settings_menu(chat_id) # Handle admin whitelist management commands def prompt_admin_for_whitelist(chat_id, action): if action == 'add': bot.send_message(chat_id, "Введите ID пользователя, которого хотите добавить в белый список") bot.register_next_step_handler_by_chat_id(chat_id, process_add_whitelist) elif action == 'remove': bot.send_message(chat_id, "Введите ID пользователя, которого хотите удалить из белого списка") bot.register_next_step_handler_by_chat_id(chat_id, process_remove_whitelist) def process_add_whitelist(message): chat_id = message.chat.id try: new_chat_id = int(message.text.split()[0]) add_to_whitelist(new_chat_id) bot.send_message(chat_id, f"Chat ID {new_chat_id} добавлен в белый список.") app.logger.info(f"Admin {chat_id} added {new_chat_id} to the whitelist.") log_user_event(new_chat_id, "N/A", f"Added to whitelist by {chat_id} (@{message.from_user.username})") except (IndexError, ValueError): bot.send_message(chat_id, "Неверный формат. Используйте: ") show_settings_menu(chat_id) def process_remove_whitelist(message): chat_id = message.chat.id try: remove_chat_id = int(message.text.split()[0]) remove_from_whitelist(remove_chat_id) bot.send_message(chat_id, f"Chat ID {remove_chat_id} удален из белого списка.") app.logger.info(f"Admin {chat_id} removed {remove_chat_id} from the whitelist.") log_user_event(remove_chat_id, "N/A", f"Removed from whitelist by {chat_id} (@{message.from_user.username})") except (IndexError, ValueError): bot.send_message(chat_id, "Неверный формат. Используйте: ") show_settings_menu(chat_id) # Handle displaying active subscriptions for a user def handle_my_subscriptions(message): chat_id = message.chat.id if not is_whitelisted(chat_id): bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") app.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: 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, "Вы не авторизованы для использования этого бота.") app.logger.info(f"Unauthorized access attempt by {chat_id}") return regions = get_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) @app.route('/webhook', methods=['POST']) def webhook(): data = request.get_json() app.logger.info(f"Received data: {data}") event_hash = hash_data(data) with db_lock: conn = sqlite3.connect('telezab.db') 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) 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)) # Fetch chat_ids to send the alert region_id = data.get("region") query = 'SELECT chat_id, username FROM subscriptions WHERE region_id = ? AND active = TRUE AND skip = 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_active = cursor.fetchone()[0] if region_active: message = format_message(data) for chat_id, username in results: try: app.logger.debug(f"Sending message: {message} to chat_id={chat_id}, username={username}") bot.send_message(chat_id, message) app.logger.info(f"Sent alert to {chat_id} ({username}) for region {region_id}") except telebot.apihelper.ApiTelegramException as e: app.logger.error(f"Failed to send message to {chat_id} ({username}): {e}") except Exception as e: app.logger.error(f"Error sending message to {chat_id} ({username}): {e}") conn.commit() conn.close() return jsonify({"status": "success"}), 200 def format_message(data): return (f"Zabbix Alert\n" f"Host: {data['host']}\n" f"Item: {data['item']}\n" f"Trigger: {data['trigger']}\n" f"Value: {data['value']}") def run_polling(): bot.polling(none_stop=True, interval=0) if __name__ == '__main__': init_db() # Start Flask app in a separate thread Thread(target=app.run, kwargs={'port': 5000, 'use_reloader': False, 'debug': True}, daemon=True).start() # Start bot polling run_polling()