Telezab/telezab.py
2024-07-09 13:47:06 +05:00

513 lines
22 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.

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 <chat_id>")
# 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 <chat_id>")
# 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 <region_id> <region_name>")
# 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 <region_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 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()