From f5ae48a7e935ee9c8048282cc0729f1c47133b32 Mon Sep 17 00:00:00 2001 From: UdoChudo Date: Tue, 9 Jul 2024 13:47:06 +0500 Subject: [PATCH] Initial commit --- .gitignore | 3 + requests.txt | 23 +++ telezab.py | 512 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 538 insertions(+) create mode 100644 .gitignore create mode 100644 requests.txt create mode 100644 telezab.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f3bc3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/telezab.db +/.env +/.idea \ No newline at end of file diff --git a/requests.txt b/requests.txt new file mode 100644 index 0000000..4fc9a12 --- /dev/null +++ b/requests.txt @@ -0,0 +1,23 @@ +anyio==4.4.0 +blinker==1.8.2 +certifi==2024.6.2 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +Flask==3.0.3 +h11==0.14.0 +httpcore==1.0.5 +httpx==0.27.0 +idna==3.7 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +packaging==24.1 +pyTelegramBotAPI==4.19.2 +python-dotenv==1.0.1 +python-telegram-bot==21.3 +requests==2.32.3 +sniffio==1.3.1 +telebot==0.0.5 +urllib3==2.2.2 +Werkzeug==3.0.3 diff --git a/telezab.py b/telezab.py new file mode 100644 index 0000000..c1377ba --- /dev/null +++ b/telezab.py @@ -0,0 +1,512 @@ +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 +import sqlite3 +import time +import re +# 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() + +# 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 + cursor.execute('''CREATE TABLE IF NOT EXISTS subscriptions ( + chat_id INTEGER, + region_id TEXT, + username TEXT, + 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 + cursor.execute('''CREATE TABLE IF NOT EXISTS regions ( + region_id TEXT PRIMARY KEY, + region_name 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') + 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.skip = FALSE + ''', (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]) + +# Handle /start command +@bot.message_handler(commands=['start']) +def handle_start(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" + + bot.send_message(chat_id, "Выполните комманду /register для начала работы") + app.logger.info(f"User {chat_id} ({username}) started receiving alerts.") + +# Handle /stop command to stop receiving messages +@bot.message_handler(commands=['stop']) +def handle_stop(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 + + with db_lock: + conn = sqlite3.connect('telezab.db') + cursor = conn.cursor() + query = 'DELETE FROM subscriptions WHERE chat_id = ?' + app.logger.debug(f"Executing query: {query} with chat_id={chat_id}") + cursor.execute(query, (chat_id,)) + conn.commit() + conn.close() + bot.send_message(chat_id, "Вы перестали получать события Zabbix'а :c") + app.logger.info(f"User {chat_id} stopped receiving alerts.") + + +# 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, "Действие отменено.") + return + + region_ids = message.text.split(',') + # Проверка формата ввода + if not all(re.match(r'^\d+$', region_id.strip()) for region_id in region_ids): + bot.send_message(chat_id, "Неверный формат команды. Пожалуйста, введите только цифры, разделенные запятыми.") + bot.register_next_step_handler(message, process_subscription, chat_id, username) + return + + invalid_regions = [] + already_subscribed = [] + with db_lock: + conn = sqlite3.connect('telezab.db') + cursor = conn.cursor() + for region_id in region_ids: + region_id = region_id.strip() + # Проверка существования региона в таблице regions + cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,)) + if cursor.fetchone()[0] == 0: + invalid_regions.append(region_id) + continue + + # Проверка существующей подписки + cursor.execute('SELECT COUNT(*) FROM subscriptions WHERE chat_id = ? AND region_id = ?', (chat_id, region_id)) + if cursor.fetchone()[0] > 0: + already_subscribed.append(region_id) + continue + + query = 'INSERT OR IGNORE INTO subscriptions (chat_id, region_id, username) VALUES (?, ?, ?)' + 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)) + + conn.commit() + conn.close() + + if invalid_regions: + bot.send_message(chat_id, f"Следующие регионы не существуют: {', '.join(invalid_regions)}. Пожалуйста, проверьте и введите снова.") + bot.register_next_step_handler(message, process_subscription, chat_id, username) + return + + if already_subscribed: + bot.send_message(chat_id, f"Вы уже подписаны на следующие регионы: {', '.join(already_subscribed)}.") + if len(already_subscribed) == len(region_ids): + bot.register_next_step_handler(message, process_subscription, chat_id, username) + return + + subscribed_regions = [region_id for region_id in region_ids if region_id not in invalid_regions and region_id not in already_subscribed] + if subscribed_regions: + bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subscribed_regions)} оформлена.") + app.logger.info(f"User {chat_id} ({username}) subscribed to regions: {', '.join(subscribed_regions)}.") + else: + bot.send_message(chat_id, "Не удалось оформить подписку на указанные регионы. Пожалуйста, попробуйте снова.") + bot.register_next_step_handler(message, process_subscription, chat_id, username) + + +# 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, "Действие отменено.") + return + + region_ids = message.text.split(',') + # Проверка формата ввода + if not all(re.match(r'^\d+$', region_id.strip()) for region_id in region_ids): + bot.send_message(chat_id, "Неверный формат команды. Пожалуйста, введите только цифры, разделенные запятыми.") + bot.register_next_step_handler(message, process_unsubscription, chat_id) + return + + invalid_unsubscriptions = [] + valid_unsubscriptions = [] + with db_lock: + conn = sqlite3.connect('telezab.db') + cursor = conn.cursor() + for region_id in region_ids: + region_id = region_id.strip() + # Проверка существования подписки + cursor.execute('SELECT COUNT(*) FROM subscriptions WHERE chat_id = ? AND region_id = ?', (chat_id, region_id)) + if cursor.fetchone()[0] == 0: + invalid_unsubscriptions.append(region_id) + else: + valid_unsubscriptions.append(region_id) + query = 'DELETE FROM subscriptions 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() + + if invalid_unsubscriptions: + bot.send_message(chat_id, f"Вы не подписаны на следующие регионы: {', '.join(invalid_unsubscriptions)}.") + if valid_unsubscriptions: + bot.send_message(chat_id, f"Отписка от регионов: {', '.join(valid_unsubscriptions)} выполнена.") + app.logger.info(f"User {chat_id} unsubscribed from regions: {', '.join(valid_unsubscriptions)}.") + if not invalid_unsubscriptions and not valid_unsubscriptions: + bot.send_message(chat_id, "Не удалось выполнить отписку. Пожалуйста, попробуйте снова.") + bot.register_next_step_handler(message, process_unsubscription, chat_id) + + +# Handle /help command to provide instructions +@bot.message_handler(commands=['help']) +def handle_help(message): + help_text = ( + "/subscribe - Подписаться на рассылку событий по региону.\n" + "/unsubscribe - Отписаться от рассылки событий по региону.\n" + "/register - Запросить регистрацию в боте" + ) + bot.send_message(message.chat.id, help_text) + +# 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" + + bot.send_message(chat_id, f"Your chat ID is {chat_id} and your username is {username}. Requesting admin approval...") + + for admin_chat_id in ADMIN_CHAT_IDS: + bot.send_message( + admin_chat_id, + f"User {username} ({chat_id}) is requesting to register.\n" + f"Do you approve this action?\n" + f"/add_whitelist {chat_id}" + ) + app.logger.info(f"User {chat_id} ({username}) requested registration.") + +# Handle /add_whitelist command to add a user to the whitelist (Admin only) +@bot.message_handler(commands=['add_whitelist']) +def handle_add_whitelist(message): + chat_id = message.chat.id + if str(chat_id) not in ADMIN_CHAT_IDS: + bot.send_message(chat_id, "Вы не авторизованы для использования этой команды.") + app.logger.info(f"Unauthorized admin command attempt by {chat_id}") + return + + try: + new_chat_id = int(message.text.split()[1]) + add_to_whitelist(new_chat_id) + bot.send_message(chat_id, f"Chat ID {new_chat_id} added to the whitelist.") + app.logger.info(f"Admin {chat_id} added {new_chat_id} to the whitelist.") + except (IndexError, ValueError): + bot.send_message(chat_id, "Invalid command format. Use /add_whitelist ") + +# Handle /remove_whitelist command to remove a user from the whitelist (Admin only) +@bot.message_handler(commands=['remove_whitelist']) +def handle_remove_whitelist(message): + chat_id = message.chat.id + if str(chat_id) not in ADMIN_CHAT_IDS: + bot.send_message(chat_id, "Вы не авторизованы для использования этой команды.") + app.logger.info(f"Unauthorized admin command attempt by {chat_id}") + return + + try: + remove_chat_id = int(message.text.split()[1]) + remove_from_whitelist(remove_chat_id) + bot.send_message(chat_id, f"Chat ID {remove_chat_id} removed from the whitelist.") + app.logger.info(f"Admin {chat_id} removed {remove_chat_id} from the whitelist.") + except (IndexError, ValueError): + bot.send_message(chat_id, "Invalid command format. Use /remove_whitelist ") + +# Handle /add_region command to add a new region (Admin only) +@bot.message_handler(commands=['add_region']) +def handle_add_region(message): + chat_id = message.chat.id + if str(chat_id) not in ADMIN_CHAT_IDS: + bot.send_message(chat_id, "Вы не авторизованы для использования этой команды.") + app.logger.info(f"Unauthorized admin command attempt by {chat_id}") + return + + try: + region_id, region_name = message.text.split()[1], ' '.join(message.text.split()[2:]) + with db_lock: + conn = sqlite3.connect('telezab.db') + cursor = conn.cursor() + 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)) + conn.commit() + conn.close() + bot.send_message(chat_id, f"Region {region_id} - {region_name} added.") + app.logger.info(f"Admin {chat_id} added region {region_id} - {region_name}.") + except (IndexError, ValueError): + bot.send_message(chat_id, "Invalid command format. Use /add_region ") + +# Handle /remove_region command to remove a region (Admin only) +@bot.message_handler(commands=['remove_region']) +def handle_remove_region(message): + chat_id = message.chat.id + if str(chat_id) not in ADMIN_CHAT_IDS: + bot.send_message(chat_id, "Вы не авторизованы для использования этой команды.") + app.logger.info(f"Unauthorized admin command attempt by {chat_id}") + return + + try: + region_id = message.text.split()[1] + with db_lock: + conn = sqlite3.connect('telezab.db') + cursor = conn.cursor() + query = 'DELETE FROM regions WHERE region_id = ?' + app.logger.debug(f"Executing query: {query} with region_id={region_id}") + cursor.execute(query, (region_id,)) + query = 'UPDATE subscriptions SET skip = TRUE WHERE region_id = ?' + 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 {region_id} removed and all subscriptions updated.") + app.logger.info(f"Admin {chat_id} removed region {region_id} and updated subscriptions.") + except IndexError: + bot.send_message(chat_id, "Invalid command format. Use /remove_region ") + +@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 skip = FALSE' + app.logger.debug(f"Executing query: {query} with region_id={region_id}") + cursor.execute(query, (region_id,)) + results = cursor.fetchall() + + 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, 'debug': False}, daemon=True).start() + + # Start bot polling + run_polling()