diff --git a/.dockerignore b/.dockerignore index bb2e957..e776b30 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,4 @@ /db/ /db/telezab.db /trash/ +/venv3.12.3/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7652792..9537578 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /logs/error.log /db/ /db/telezab.db +/venv3.12.3/ \ No newline at end of file diff --git a/backend_bot.py b/backend_bot.py index 373ad3a..03b935c 100644 --- a/backend_bot.py +++ b/backend_bot.py @@ -1,23 +1,21 @@ import sqlite3 - import telebot - import telezab from backend_locks import db_lock, bot from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \ get_user_subscribed_regions from config import DB_PATH from telezab import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button -from utils import show_main_menu, show_settings_menu +from utilities.telegram_utilities import show_main_menu, show_settings_menu def handle_main_menu(message, chat_id, text): """Обработка команд в главном меню.""" if text == 'Регистрация': - telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + telezab.state.set_state(chat_id, "REGISTRATION") telezab.handle_register(message) elif text == 'Настройки': - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") telezab.show_settings_menu(chat_id) elif text == 'Помощь': telezab.handle_help(message) @@ -32,10 +30,10 @@ def handle_settings_menu(message, chat_id, text): """Обработка команд в меню настроек.""" admins_list = get_admins() if text.lower() == 'подписаться': - telezab.user_state_manager.set_state(chat_id, "SUBSCRIBE") + telezab.state.set_state(chat_id, "SUBSCRIBE") handle_subscribe_button(message) elif text.lower() == 'отписаться': - telezab.user_state_manager.set_state(chat_id, "UNSUBSCRIBE") + telezab.state.set_state(chat_id, "UNSUBSCRIBE") handle_unsubscribe_button(message) elif text.lower() == 'мои подписки': handle_my_subscriptions_button(message) @@ -44,7 +42,7 @@ def handle_settings_menu(message, chat_id, text): elif text.lower() == "режим уведомлений": handle_notification_mode_button(message) elif text.lower() == 'назад': - telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") + telezab.state.set_state(chat_id, "MAIN_MENU") show_main_menu(chat_id) else: bot.send_message(chat_id, "Команда не распознана.") @@ -75,7 +73,7 @@ def process_subscription_button(message, chat_id, username): invalid_regions = [] if message.text.lower() == 'отмена': bot.send_message(chat_id, "Действие отменено.") - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") return show_settings_menu(chat_id) if not all(part.strip().isdigit() for part in message.text.split(',')): markup = telebot.types.InlineKeyboardMarkup() @@ -108,7 +106,7 @@ def process_subscription_button(message, chat_id, username): f"Регион с ID {', '.join(invalid_regions)} не существует. Введите корректные номера или 'отмена'.") bot.send_message(chat_id, f"Подписка на регионы: {', '.join(subbed_regions)} оформлена.") log_user_event(chat_id, username, f"Subscribed to regions: {', '.join(subbed_regions)}") - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") show_settings_menu(chat_id) @@ -117,7 +115,7 @@ def handle_unsubscribe_button(message): if not is_whitelisted(chat_id): bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") telebot.logger.info(f"Unauthorized access attempt by {chat_id}") - telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + telezab.state.set_state(chat_id, "REGISTRATION") return show_main_menu(chat_id) username = message.from_user.username if username: @@ -129,7 +127,7 @@ def handle_unsubscribe_button(message): if not user_regions: bot.send_message(chat_id, "Вы не подписаны ни на один регион.") - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") return show_settings_menu(chat_id) regions_list = format_regions_list(user_regions) markup = telebot.types.InlineKeyboardMarkup() @@ -147,7 +145,7 @@ def process_unsubscription_button(message, chat_id, username): markup.add(telebot.types.InlineKeyboardButton(text="Отмена", callback_data=f"cancel_action")) if message.text.lower() == 'отмена': bot.send_message(chat_id, "Действие отменено.") - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") return show_settings_menu(chat_id) # Проверка, что введённая строка содержит только цифры и запятые if not all(part.strip().isdigit() for part in message.text.split(',')): @@ -173,5 +171,5 @@ def process_unsubscription_button(message, chat_id, username): bot.send_message(chat_id, f"Регион с ID {', '.join(invalid_regions)} не найден в ваших подписках.") bot.send_message(chat_id, f"Отписка от регионов: {', '.join(unsubbed_regions)} выполнена.") log_user_event(chat_id, username, f"Unsubscribed from regions: {', '.join(unsubbed_regions)}") - telezab.user_state_manager.set_state(chat_id, "SETTINGS_MENU") + telezab.state.set_state(chat_id, "SETTINGS_MENU") show_settings_menu(chat_id) diff --git a/backend_flask.py b/backend_flask.py index a48cb3e..cc88a4f 100644 --- a/backend_flask.py +++ b/backend_flask.py @@ -2,18 +2,37 @@ import logging import sqlite3 import telebot -from flask import Flask, request, jsonify, render_template +from flask import Flask, request, jsonify, render_template, flash, redirect, url_for +from flask_ldap3_login.forms import LDAPLoginForm +from flask_login import login_manager, login_user, logout_user, UserMixin + +from frontend.dashboard import bp_dashboard, bp_api import backend_bot import bot_database import telezab -import utils +import utilities.telegram_utilities as telegram_util from backend_locks import db_lock from config import BASE_URL, DB_PATH -from utils import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email +from utilities.telegram_utilities import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates') # app.register_blueprint(webui) +app.secret_key = "supersecretkey" +app.register_blueprint(bp_dashboard) +app.register_blueprint(bp_api) +# +# # Инициализация менеджеров +# ldap_manager = LDAP3LoginManager(app) +# login_manager = LoginManager(app) +# login_manager.login_view = "login" + +# Пользовательский класс +class User(UserMixin): + def __init__(self, dn, username): + self.id = dn + self.username = username + # Настройка уровня логирования для Flask app.logger.setLevel(logging.INFO) @@ -26,9 +45,9 @@ def webhook(): data = request.get_json() app.logger.info(f"Получены данные: {data}") - # Генерация хеша события и логирование - event_hash = bot_database.hash_data(data) - app.logger.debug(f"Сгенерирован хеш для события: {event_hash}") + # # Генерация хеша события и логирование + # event_hash = bot_database.hash_data(data) + # app.logger.debug(f"Сгенерирован хеш для события: {event_hash}") # Работа с базой данных в блоке синхронизации with db_lock: @@ -82,7 +101,7 @@ def webhook(): app.logger.info( f"Формирование сообщения для пользователя {username} (chat_id={chat_id}) [{formatted_message}]") try: - from rabbitmq import send_to_queue + from utilities.rabbitmq import send_to_queue send_to_queue({'chat_id': chat_id, 'username': username, 'message': message}) app.logger.debug(f"Сообщение поставлено в очередь для {chat_id} (@{username})") except Exception as e: @@ -93,8 +112,8 @@ def webhook(): if undelivered: query = 'INSERT OR IGNORE INTO events (hash, data, delivered) VALUES (?, ?, ?)' app.logger.debug( - f"Сохранение события в базе данных: {query} (hash={event_hash}, delivered={False})") - cursor.execute(query, (event_hash, str(data), False)) + f"Сохранение события в базе данных: {query} (delivered={False})") + cursor.execute(query, (str(data), False)) # Коммитим изменения в базе данных conn.commit() @@ -152,11 +171,11 @@ def add_user(): if success: # INFO: Пользователь успешно добавлен в whitelist app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.") - telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") + telezab.state.set_state(chat_id, "MAIN_MENU") # DEBUG: Показ основного меню пользователю app.logger.debug(f"Отображение основного меню для пользователя с chat_id {chat_id}") - utils.show_main_menu(chat_id) + telegram_util.show_main_menu(chat_id) return jsonify( {"status": "success", "msg": f"User {telegram_id} with {user_email} added successfully"}), 200 else: @@ -238,76 +257,71 @@ def delete_user(): app.logger.debug(f"Соединение с базой данных закрыто") -@app.route(BASE_URL + '/users/get', methods=['GET']) -def get_users(): - try: - # INFO: Запрос на получение списка пользователей - app.logger.info("Запрос на получение информации о пользователях получен") - - with db_lock: - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - # DEBUG: Запрос данных из таблицы whitelist - app.logger.debug("Запрос данных пользователей из таблицы whitelist") - cursor.execute('SELECT * FROM whitelist') - users = cursor.fetchall() - app.logger.debug("Формирование словаря пользователей") - users_dict = {user_id: {'id': user_id, 'username': username, 'email': email, 'events': [], 'worker': '', - 'subscriptions': []} - for user_id, username, email in users} - - # DEBUG: Запрос данных событий пользователей - app.logger.debug("Запрос событий пользователей из таблицы user_events") - cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') - events = cursor.fetchall() - - # DEBUG: Обработка событий и добавление их в словарь пользователей - for chat_id, username, action, timestamp in events: - if chat_id in users_dict: - event = {'type': action, 'date': timestamp} - if "Subscribed to region" in action: - region = action.split(": ")[-1] - event['region'] = region - users_dict[chat_id]['events'].append(event) - - # DEBUG: Запрос данных подписок пользователей - app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions") - cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') - subscriptions = cursor.fetchall() - - # DEBUG: Добавление подписок к пользователям - for chat_id, region_id in subscriptions: - if chat_id in users_dict: - users_dict[chat_id]['subscriptions'].append(str(region_id)) - - # INFO: Формирование результата - app.logger.info("Формирование результата для ответа") - result = [] - for user in users_dict.values(): - ordered_user = { - 'email': user['email'], - 'username': user['username'], - 'id': user['id'], - 'worker': user['worker'], - 'events': user['events'], - 'subscriptions': ', '.join(user['subscriptions']) - } - result.append(ordered_user) - - # INFO: Успешная отправка данных пользователей - app.logger.info("Информация о пользователях успешно отправлена") - return jsonify(result) - - except Exception as e: - # ERROR: Ошибка при получении информации о пользователях - app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}") - return jsonify({'status': 'error', 'message': str(e)}), 500 - - -@app.route(BASE_URL + '/users', methods=['GET']) -def view_users(): - return render_template('users.html') +# @app.route(BASE_URL + '/users/get', methods=['GET']) +# def get_users(): +# try: +# # INFO: Запрос на получение списка пользователей +# app.logger.info("Запрос на получение информации о пользователях получен") +# +# with db_lock: +# conn = sqlite3.connect(DB_PATH) +# cursor = conn.cursor() +# +# # DEBUG: Запрос данных из таблицы whitelist +# app.logger.debug("Запрос данных пользователей из таблицы whitelist") +# cursor.execute('SELECT * FROM whitelist') +# users = cursor.fetchall() +# app.logger.debug("Формирование словаря пользователей") +# users_dict = {user_id: {'id': user_id, 'username': username, 'email': email, 'events': [], 'worker': '', +# 'subscriptions': []} +# for user_id, username, email in users} +# +# # DEBUG: Запрос данных событий пользователей +# app.logger.debug("Запрос событий пользователей из таблицы user_events") +# cursor.execute('SELECT chat_id, username, action, timestamp FROM user_events') +# events = cursor.fetchall() +# +# # DEBUG: Обработка событий и добавление их в словарь пользователей +# for chat_id, username, action, timestamp in events: +# if chat_id in users_dict: +# event = {'type': action, 'date': timestamp} +# if "Subscribed to region" in action: +# region = action.split(": ")[-1] +# event['region'] = region +# users_dict[chat_id]['events'].append(event) +# +# # DEBUG: Запрос данных подписок пользователей +# app.logger.debug("Запрос активных подписок пользователей из таблицы subscriptions") +# cursor.execute('SELECT chat_id, region_id FROM subscriptions WHERE active = 1') +# subscriptions = cursor.fetchall() +# +# # DEBUG: Добавление подписок к пользователям +# for chat_id, region_id in subscriptions: +# if chat_id in users_dict: +# users_dict[chat_id]['subscriptions'].append(str(region_id)) +# +# # INFO: Формирование результата +# app.logger.info("Формирование результата для ответа") +# result = [] +# for user in users_dict.values(): +# ordered_user = { +# 'email': user['email'], +# 'username': user['username'], +# 'id': user['id'], +# 'worker': user['worker'], +# 'events': user['events'], +# 'subscriptions': ', '.join(user['subscriptions']) +# } +# result.append(ordered_user) +# +# # INFO: Успешная отправка данных пользователей +# app.logger.info("Информация о пользователях успешно отправлена") +# return jsonify(result) +# +# except Exception as e: +# # ERROR: Ошибка при получении информации о пользователях +# app.logger.error(f"Ошибка при получении информации о пользователях: {str(e)}") +# return jsonify({'status': 'error', 'message': str(e)}), 500 @app.route(BASE_URL + '/debug/flask', methods=['POST']) diff --git a/backend_zabbix.py b/backend_zabbix.py index 4ad60cf..387d469 100644 --- a/backend_zabbix.py +++ b/backend_zabbix.py @@ -8,7 +8,7 @@ from pyzabbix import ZabbixAPI import backend_bot from config import ZABBIX_URL, ZABBIX_API_TOKEN -from utils import show_main_menu +from utilities.telegram_utilities import show_main_menu def get_triggers_for_group(chat_id, group_id): diff --git a/config.py b/config.py index d77fc79..de0bd6c 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,14 @@ DB_PATH = 'db/telezab.db' SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru" BASE_URL = '/telezab' RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') -RABBITMQ_QUEUE = 'telegram_notifications' RABBITMQ_LOGIN = os.getenv('RABBITMQ_LOGIN') RABBITMQ_PASS = os.getenv('RABBITMQ_PASS') +RABBITMQ_QUEUE = 'telegram_notifications' RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" + +import os + +class Config: + SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_PATH}' + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key' \ No newline at end of file diff --git a/frontend/dashboard.py b/frontend/dashboard.py new file mode 100644 index 0000000..895e7c6 --- /dev/null +++ b/frontend/dashboard.py @@ -0,0 +1,177 @@ +from flask import Blueprint, render_template, jsonify, request, redirect, url_for +from flask_login import login_required +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine, text +from config import DB_PATH, BASE_URL +from .models import Region # Импортируем модель региона + +# Создаём Blueprint +bp_dashboard = Blueprint('dashboard', __name__, url_prefix='/telezab/') +bp_api = Blueprint('api', __name__, url_prefix='/telezab/rest/api') + +db_engine = create_engine(f'sqlite:///{DB_PATH}') +Session = sessionmaker(bind=db_engine) + +# Роуты для отображения страниц +@bp_dashboard.route('/') +# @login_required +def dashboard(): + return render_template('index.html') + +@bp_dashboard.route('/users') +# @login_required +def users_page(): + return render_template('users.html') + +@bp_dashboard.route('/logs') +# @login_required +def logs_page(): + return render_template('logs.html') + +@bp_dashboard.route('/regions') +# @login_required +def regions_page(): + return render_template('regions.html') + +# Роуты для API +@bp_api.route('/users', methods=['GET']) +def get_users(): + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 20, type=int) + + session = Session() + query = text(""" + SELECT w.chat_id, w.username, w.user_email ,s.region_id, s.disaster_only + FROM whitelist w + LEFT JOIN subscriptions s ON w.chat_id = s.chat_id AND s.active = 1 + """) + + users = session.execute(query).fetchall() + + # Если users пустые, выводим сообщение в консоль + if not users: + print("No users found") + + # Группируем подписки по chat_id + user_dict = {} + for u in users: + chat_id = u[0] + if chat_id not in user_dict: + disaster_only_text = "Только критические уведомления" if u[4] == 1 else "Все уведомления" + # is_blocked = "Заблокирован" if u[5] == 1 else "Активен" + user_dict[chat_id] = { + 'id': u[0], + 'username': u[1], + 'email': u[2], + 'subscriptions': [], + 'disaster_only': disaster_only_text, + # 'status': is_blocked + } + if u[3]: + user_dict[chat_id]['subscriptions'].append(u[3]) + + users_list = list(user_dict.values()) + total_users = len(users_list) + total_pages = (total_users + per_page - 1) // per_page + start = (page - 1) * per_page + end = start + per_page + users_page = users_list[start:end] + + session.close() + + return jsonify({ + 'users': users_page, + 'total_users': total_users, + 'total_pages': total_pages, + 'current_page': page, + 'per_page': per_page + }) + +@bp_api.route('/regions', methods=['GET', 'POST']) +def manage_regions(): + session = Session() + if request.method == 'POST': + data = request.json + region_id = data.get('region_id') + name = data.get('name') + active = data.get('active', True) + + region = Region(region_id=region_id, region_name=name, active=active) + session.add(region) + session.commit() + return jsonify({'status': 'success'}) + + regions = session.query(Region).all() + session.close() + return jsonify([{'region_id': r.region_id, 'name': r.region_name, 'active': r.active} for r in regions]) + +@bp_api.route('/regions/', methods=['PUT', 'DELETE']) +def edit_region(region_id): + session = Session() + region = session.query(Region).filter_by(region_id=region_id).first() + + if request.method == 'PUT': + data = request.json + region.region_name = data.get('name', region.region_name) + region.active = data.get('active', region.active) + session.commit() + session.close() + return jsonify({'status': 'updated'}) + + elif request.method == 'DELETE': + session.delete(region) + session.commit() + session.close() + return jsonify({'status': 'deleted'}) + +@bp_api.route('/users/', methods=['GET']) +def get_user(user_id): + session = Session() + user = session.execute(text("SELECT * FROM whitelist WHERE chat_id = :id"), {'id': user_id}).fetchone() + session.close() + if not user: + return jsonify({'error': 'Пользователь не найден'}), 404 + return jsonify({'id': user.chat_id, 'username': user.username, 'email': user.user_email, 'blocked': user.is_blocked}) + +# @bp_api.route('/users//block', methods=['POST']) +# def block_user(user_id): +# session = Session() +# session.execute(text("UPDATE whitelist SET is_blocked = False WHERE chat_id = :id"), {'id': user_id}) +# session.commit() +# session.close() +# return jsonify({'status': 'updated'}) +@bp_api.route('/users//block', methods=['POST']) +def block_user(user_id): + session = Session() + + # Получаем текущий статус блокировки пользователя + result = session.execute(text("SELECT is_blocked FROM whitelist WHERE chat_id = :id"), {'id': user_id}).fetchone() + + if result: + is_blocked = result[0] # Текущее значение блокировки + + # Если пользователь заблокирован, разблокируем его, если разблокирован - блокируем + new_status = not is_blocked + + # Обновляем статус блокировки в базе данных + session.execute( + text("UPDATE whitelist SET is_blocked = :new_status WHERE chat_id = :id"), + {'new_status': new_status, 'id': user_id} + ) + session.commit() + + session.close() + return jsonify({'status': 'updated', 'new_status': new_status}) + else: + session.close() + return jsonify({'status': 'error', 'message': 'User not found'}), 404 + + + +@bp_api.route('/users/', methods=['DELETE']) +def delete_user(user_id): + session = Session() + session.execute(text("DELETE FROM whitelist WHERE chat_id = :id"), {'id': user_id}) + session.commit() + session.close() + return jsonify({'status': 'deleted'}) diff --git a/frontend/models.py b/frontend/models.py new file mode 100644 index 0000000..85841e7 --- /dev/null +++ b/frontend/models.py @@ -0,0 +1,19 @@ +from utilities.database import db # Импортируем db из backend_flask.py + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + is_blocked = db.Column(db.Boolean, default=False) + actions = db.Column(db.String(500)) + subscriptions = db.Column(db.String(500)) + +class Region(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), nullable=False) + active = db.Column(db.Boolean, default=True) + +class Log(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), nullable=False) + action = db.Column(db.String(500), nullable=False) + timestamp = db.Column(db.DateTime, default=db.func.current_timestamp()) \ No newline at end of file diff --git a/frontend/routes/auth.py b/frontend/routes/auth.py new file mode 100644 index 0000000..ec6e235 --- /dev/null +++ b/frontend/routes/auth.py @@ -0,0 +1,14 @@ +from flask import Blueprint, render_template, request, redirect, url_for + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + # Обработка логики авторизации + pass + return render_template('login.html') + +@auth_bp.route('/') +def index(): + return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/frontend/routes/logs.py b/frontend/routes/logs.py new file mode 100644 index 0000000..4d1e250 --- /dev/null +++ b/frontend/routes/logs.py @@ -0,0 +1,9 @@ +from flask import Blueprint, render_template +from frontend.models import Log + +logs_bp = Blueprint('logs', __name__) + +@logs_bp.route('/logs') +def logs(): + logs = Log.query.all() + return render_template('logs.html', logs=logs) \ No newline at end of file diff --git a/frontend/routes/regions.py b/frontend/routes/regions.py new file mode 100644 index 0000000..b2ac6b3 --- /dev/null +++ b/frontend/routes/regions.py @@ -0,0 +1,9 @@ +from flask import Blueprint, render_template +from frontend.models import Region + +regions_bp = Blueprint('regions', __name__) + +@regions_bp.route('/regions') +def regions(): + regions = Region.query.all() + return render_template('regions.html', regions=regions) \ No newline at end of file diff --git a/frontend/routes/users.py b/frontend/routes/users.py new file mode 100644 index 0000000..5eb3518 --- /dev/null +++ b/frontend/routes/users.py @@ -0,0 +1,9 @@ +from flask import Blueprint, render_template +from frontend.models import User + +users_bp = Blueprint('users', __name__) + +@users_bp.route('/users') +def users(): + user = User.query.all() + return render_template('users.html', users=user) \ No newline at end of file diff --git a/region_api.py b/region_api.py deleted file mode 100644 index 10ac49d..0000000 --- a/region_api.py +++ /dev/null @@ -1,107 +0,0 @@ -import sqlite3 -import logging -from threading import Lock - -db_lock = Lock() - -# Инициализируем логгер -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования - - -class RegionAPI: - def __init__(self, db_path): - self.db_path = db_path - - def add_region(self, region_id: int, region_name: str): - logger.info(f"Запрос на добавление региона: id={region_id}, name={region_name}") - - # Проверка валидности region_id - if not str(region_id).isdigit(): - logger.error(f"region_id {region_id} не является числом.") - return {"status": "failure", "message": "Region_id must be digit only"} - - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - logger.debug(f"Проверка существования региона с id={region_id}") - cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,)) - count = cursor.fetchone()[0] - - if count == 0: - # Добавляем новый регион - cursor.execute('INSERT INTO regions (region_id, region_name, active) VALUES (?, ?, 1)', - (region_id, region_name)) - conn.commit() - logger.info(f"Регион с id={region_id} успешно добавлен.") - return {"status": "success", "message": "Region added successfully"} - else: - logger.warning(f"Регион с id={region_id} уже существует.") - return {"status": "error", "message": "Region already exists"} - - def remove_region(self, region_id): - logger.info(f"Запрос на удаление региона: id={region_id}") - - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - logger.debug(f"Проверка существования региона с id={region_id}") - cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,)) - count = cursor.fetchone()[0] - - if count == 0: - logger.warning(f"Регион с id={region_id} не найден.") - return {"status": "error", "message": "Region not found"} - else: - cursor.execute('DELETE FROM regions WHERE region_id = ?', (region_id,)) - conn.commit() - logger.info(f"Регион с id={region_id} успешно удалён.") - return {"status": "success", "message": "Region removed successfully"} - - def get_regions(self): - logger.info("Запрос на получение списка регионов.") - - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - logger.debug("Извлечение данных из таблицы regions.") - cursor.execute('SELECT region_id, region_name, active FROM regions') - regions = cursor.fetchall() - logger.info(f"Получено {len(regions)} регионов.") - return [{"region_id": r[0], "region_name": r[1], "regions_active": r[2]} for r in regions] - - def change_region_status(self, region_id, active): - logger.info(f"Запрос на изменение статуса региона: id={region_id}, статус={active}") - - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - logger.debug(f"Проверка существования региона с id={region_id}") - cursor.execute('SELECT COUNT(*) FROM regions WHERE region_id = ?', (region_id,)) - count = cursor.fetchone()[0] - - if count == 0: - logger.warning(f"Регион с id={region_id} не найден.") - return {"status": "error", "message": "Region not found"} - else: - cursor.execute('UPDATE regions SET active = ? WHERE region_id = ?', (active, region_id)) - conn.commit() - logger.info(f"Статус региона с id={region_id} успешно изменён.") - return {"status": "success", "message": "Region status updated successfully"} - - def update_region_status(self, region_id, active): - logger.info(f"Запрос на обновление статуса региона: id={region_id}, активность={active}") - - with db_lock, sqlite3.connect(self.db_path) as conn: - cursor = conn.cursor() - - # Проверяем существование региона - logger.debug(f"Проверка существования региона с id={region_id}") - cursor.execute("SELECT region_name FROM regions WHERE region_id = ?", (region_id,)) - result = cursor.fetchone() - if not result: - logger.warning(f"Регион с id={region_id} не найден.") - return {"status": "error", "message": "Регион не найден"} - - # Обновляем статус активности региона - cursor.execute("UPDATE regions SET active = ? WHERE region_id = ?", (int(active), region_id)) - conn.commit() - action = "Активирован" if active else "Отключён" - logger.info(f"Регион с id={region_id} {action}.") - return {"status": "success", "message": f"Регион {region_id} {action}"} diff --git a/requirements.txt b/requirements.txt index dcf90c6..51aba7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,10 +11,16 @@ click==8.1.8 colorama==0.4.6 exceptiongroup==1.2.2 Flask==3.1.0 +flask-ldap3-login==1.0.2 +Flask-Login==0.6.3 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.2 frozenlist==1.5.0 +greenlet==3.1.1 idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.5 +ldap3==2.9.1 MarkupSafe==3.0.2 multidict==6.1.0 packaging==24.2 @@ -22,13 +28,17 @@ pamqp==3.3.0 pika==1.3.2 pika-stubs==0.1.3 propcache==0.2.1 +pyasn1==0.6.1 pyTelegramBotAPI==4.26.0 python-dotenv==1.0.1 pytz==2025.1 pyzabbix==1.3.1 requests==2.32.3 schedule==1.2.2 +SQLAlchemy==2.0.38 telebot==0.0.5 +typing_extensions==4.12.2 urllib3==2.3.0 Werkzeug==3.1.3 +WTForms==3.2.1 yarl==1.18.3 diff --git a/static/css/users.css b/static/css/users.css index 14cc874..8b13789 100644 --- a/static/css/users.css +++ b/static/css/users.css @@ -1,25 +1 @@ -/* Добавим выравнивание и отступы для кнопок управления */ -.table-hover tbody tr td { - vertical-align: middle; -} -.table-hover tbody tr td .btn-manage { - margin-left: 20px; /* Отступ кнопки от названия региона */ -} - -th, td { - text-align: left; /* Выровнять содержимое слева */ -} - -.table-hover tbody tr { - height: 50px; /* Сделать строки таблицы выше для лучшего визуального эффекта */ -} - -.modal-footer .btn { - margin-left: 10px; -} - -/* Стили для пагинации */ -.d-flex .btn { - margin: 0 5px; -} diff --git a/static/js/users.js b/static/js/users.js index d04a140..40a7753 100644 --- a/static/js/users.js +++ b/static/js/users.js @@ -1,55 +1,173 @@ -$(document).ready(function() { - // Получаем список сотрудников - $.getJSON('/telezab/users/get', function(data) { - var userList = $('#user-list'); - userList.empty(); - data.forEach(function(user) { - var email = user.email; - var name = email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase()); - var listItem = $('
  • ').text(name).data('user', user); - userList.append(listItem); +let currentPage = 1; +let totalPages = 1; +const perPage = 20; + +// Функция загрузки пользователей +function loadUsers(page) { + if (page < 1 || page > totalPages) return; + currentPage = page; + + fetch(`/telezab/rest/api/users?page=${currentPage}&per_page=${perPage}`) + .then(response => response.json()) + .then(data => { + totalPages = data.total_pages; + updateUsersTable(data.users); + updatePagination(data.current_page, data.total_pages); + }) + .catch(error => { + console.error('Error fetching users:', error); + }); +} + +// Функция обновления таблицы пользователей +function updateUsersTable(users) { + const tableBody = document.getElementById('users-table').querySelector('tbody'); + tableBody.innerHTML = ''; + + users.forEach(user => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${user.id} + ${user.username} + ${user.email} + ${user.subscriptions.join(', ') || 'Нет подписок'} + ${user.disaster_only} + ${user.status} + + `; + tableBody.appendChild(row); + }); + + setupEditButtons(); +} + +// Функция для обработки кнопок "Редактировать" +function setupEditButtons() { + document.querySelectorAll(".editUserBtn").forEach(button => { + button.addEventListener("click", function () { + const userId = this.dataset.id; + openUserModal(userId); }); }); +} - // Обработчик кликов по пользователям - $('#user-list').on('click', '.list-group-item', function() { - // Удаляем активный класс у всех элементов списка - $('.list-group-item').removeClass('active'); +// Функция открытия модального окна +function openUserModal(userId) { + fetch(`/telezab/rest/api/users/${userId}`) + .then(response => response.json()) + .then(data => { + document.getElementById("userId").innerText = data.id; + document.getElementById("username").innerText = data.username; + document.getElementById("userEmail").innerText = data.email; - // Добавляем активный класс к выбранному элементу - $(this).addClass('active'); + const blockBtn = document.getElementById("toggleBlockUser"); + blockBtn.innerText = data.blocked ? "Разблокировать" : "Заблокировать"; + blockBtn.onclick = function () { + toggleUserBlock(userId); + }; - var user = $(this).data('user'); - $('#user-info').removeClass('d-none'); - $('#user-name').text(user.email.split('@')[0].replace(/\./g, ' ').replace(/\b\w/g, char => char.toUpperCase())); + document.getElementById("viewUserEvents").onclick = function () { + viewUserEvents(userId); + }; - // Отображаем регионы в одну строку - var regions = $('#user-regions'); - regions.empty(); - if (user.subscriptions) { - var subscriptions = user.subscriptions.split(',').map(function(sub) { return sub.trim(); }); - if (subscriptions.length > 0) { - regions.text(subscriptions.join(', ')); - } else { - regions.text('Нет подписок'); - } - } else { - regions.text('Нет подписок'); - } + document.getElementById("deleteUser").onclick = function () { + deleteUser(userId); + }; - // Отображаем действия - var events = $('#user-events'); - events.empty(); - if (user.events && user.events.length > 0) { - user.events.forEach(function(event) { - var eventText = event.type; - if (event.region) { - eventText += ' (Регион: ' + event.region + ')'; - } - events.append('
    ' + event.date + ' - ' + eventText + '
    '); + // Использование Bootstrap для показа модального окна + var myModal = new bootstrap.Modal(document.getElementById('userModal'), { + keyboard: false }); - } else { - events.append('
    Нет действий
    '); - } - }); + myModal.show(); // Открытие модального окна + }) + .catch(error => { + console.error('Error fetching user data:', error); + }); +} + +// Обработчик закрытия модального окна +document.querySelector(".btn-close").addEventListener("click", function () { + var myModal = new bootstrap.Modal(document.getElementById('userModal')); + myModal.hide(); // Закрытие модального окна +}); + + +// Функция для блокировки/разблокировки пользователя +function toggleUserBlock(userId) { + fetch(`/telezab/rest/api/users/${userId}/block`, { method: "POST" }) + .then(() => { + alert("Статус пользователя изменён"); + + // Скрыть модальное окно с помощью Bootstrap (это не отменяет событий закрытия) + var myModal = new bootstrap.Modal(document.getElementById('userModal')); + myModal.hide(); // Закрытие модального окна + + loadUsers(currentPage); // Перезагрузите список пользователей + }) + .catch(error => { + console.error("Ошибка при изменении статуса пользователя:", error); + }); +} + + +// Функция просмотра действий пользователя +function viewUserEvents(userId) { + window.location.href = `/telezab/rest/api/users/${userId}/events`; +} + +// Функция для удаления пользователя +function deleteUser(userId) { + if (confirm("Вы уверены, что хотите удалить пользователя?")) { + fetch(`/telezab/rest/api/users/${userId}`, { method: "DELETE" }) + .then(() => { + alert("Пользователь удалён"); + var myModal = new bootstrap.Modal(document.getElementById('userModal')); + myModal.hide(); + loadUsers(currentPage); // Перезагрузите список пользователей + }); + } +} + +window.onclick = function (event) { + if (event.target === document.getElementById("userModal")) { + $('#userModal').modal('hide'); // Закрываем модальное окно с помощью Bootstrap + } +}; + +// Функция обновления пагинации +function updatePagination(currentPage, totalPages) { + const paginationContainer = document.getElementById('pagination'); + paginationContainer.innerHTML = ''; + + const prevButton = document.createElement('li'); + prevButton.classList.add('page-item'); + prevButton.classList.toggle('disabled', currentPage === 1); + prevButton.innerHTML = `«`; + paginationContainer.appendChild(prevButton); + + for (let page = 1; page <= totalPages; page++) { + const pageItem = document.createElement('li'); + pageItem.classList.add('page-item'); + pageItem.classList.toggle('active', page === currentPage); + + const pageLink = document.createElement('a'); + pageLink.classList.add('page-link'); + pageLink.href = "#"; + pageLink.textContent = page; + pageLink.onclick = () => loadUsers(page); + + pageItem.appendChild(pageLink); + paginationContainer.appendChild(pageItem); + } + + const nextButton = document.createElement('li'); + nextButton.classList.add('page-item'); + nextButton.classList.toggle('disabled', currentPage === totalPages); + nextButton.innerHTML = `»`; + paginationContainer.appendChild(nextButton); +} + +// Запуск загрузки данных +document.addEventListener("DOMContentLoaded", () => { + loadUsers(currentPage); }); diff --git a/telezab.py b/telezab.py index dc4233f..929ac24 100644 --- a/telezab.py +++ b/telezab.py @@ -2,11 +2,9 @@ import asyncio import logging import sqlite3 from threading import Thread - import telebot from pyzabbix import ZabbixAPI from telebot import types - import backend_bot import bot_database from backend_flask import app @@ -14,13 +12,14 @@ from backend_locks import bot from backend_locks import db_lock from backend_zabbix import get_triggers_for_group, get_triggers_for_all_groups from config import * -from log_manager import LogManager -from rabbitmq import consume_from_queue -from user_state_manager import UserStateManager -from utils import show_main_menu, show_settings_menu +from utilities.log_manager import LogManager +from utilities.rabbitmq import consume_from_queue +from utilities.telegram_utilities import show_main_menu, show_settings_menu +from utilities.user_state_manager import UserStateManager + # Инициализируем класс UserStateManager -user_state_manager = UserStateManager() +state = UserStateManager() # Инициализация LogManager log_manager = LogManager(log_dir='logs', retention_days=30) @@ -31,12 +30,6 @@ telebot.logger = logging.getLogger('telebot') # Важно: вызов schedule_log_rotation для планировки ротации и архивации логов log_manager.schedule_log_rotation() - -# # Lock for database operations -# db_lock = Lock() - -# 25 messages per second - # Handle /help command to provide instructions @bot.message_handler(commands=['help']) def handle_help(message): @@ -92,7 +85,7 @@ def handle_menu_selection(message): backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.") return # Получаем текущее состояние пользователя - current_state = user_state_manager.get_state(chat_id) + current_state = state.get_state(chat_id) # Обработка команд в зависимости от состояния if current_state == "MAIN_MENU": backend_bot.handle_main_menu(message, chat_id, text) @@ -116,7 +109,7 @@ def handle_cancel_action(call): backend_bot.bot.clear_step_handler_by_chat_id(chat_id) backend_bot.bot.send_message(chat_id, f"Действие отменено") backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) - user_state_manager.set_state(chat_id, "SETTINGS_MENU") + state.set_state(chat_id, "SETTINGS_MENU") show_settings_menu(chat_id) return @@ -128,7 +121,7 @@ def handle_cancel_active_triggers(call): backend_bot.bot.clear_step_handler_by_chat_id(chat_id) backend_bot.bot.send_message(chat_id, f"Действие отменено") backend_bot.bot.edit_message_reply_markup(chat_id, message_id, reply_markup=None) - user_state_manager.set_state(chat_id, "MAIN_MENU") + state.set_state(chat_id, "MAIN_MENU") show_main_menu(chat_id) return @@ -229,7 +222,7 @@ def handle_notification_mode_selection(call): telebot.logger.info(f"Notification mode for user ({chat_id}) updated to: {mode_text}") # Логируем изменение состояния пользователя - user_state_manager.set_state(chat_id, "SETTINGS_MENU") + state.set_state(chat_id, "SETTINGS_MENU") telebot.logger.debug(f"User state for {chat_id} set to SETTINGS_MENU.") # Показываем меню настроек @@ -381,7 +374,6 @@ def run_flask(): def main(): # Инициализация базы данных bot_database.init_db() - # Запуск Flask и бота в отдельных потоках Thread(target=run_flask, daemon=True).start() Thread(target=run_polling, daemon=True).start() diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b3c277f --- /dev/null +++ b/templates/base.html @@ -0,0 +1,44 @@ + + + + + + {% block title %}Dashboard{% endblock %} + + + + + + + +
    + {% block content %}{% endblock %} +
    + + + + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..95cdcbb --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,145 @@ + + + + + + Dashboard + + + +
    +

    Панель управления

    + +

    Пользователи

    + + + + + + + + + + + + + +
    IDИмяEmailПодпискиРежим
    + + + +
    + + + +{##} + + + diff --git a/templates/index.html b/templates/index.html index e8b72f2..8728978 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,10 +1,55 @@ - - - - - Index of WebUI for Telezab - - -THIS IS WEBUI FOR TELEZAB BOT - - \ No newline at end of file +{% extends "base.html" %} + +{% block title %}Dashboard Home{% endblock %} + +{% block content %} +

    Добро пожаловать в Dashboard

    +

    Выберите один из разделов:

    + +
    + +
    +
    +
    +
    Пользователи
    +

    Просмотр и управление пользователями.

    + Перейти +
    +
    +
    + + +
    +
    +
    +
    Регионы
    +

    Просмотр и настройка регионов.

    + Перейти +
    +
    +
    + + +
    +
    +
    +
    Логи
    +

    Просмотр событий и логов.

    + Перейти +
    +
    +
    + + +
    +
    +
    +
    Создание чатов Teams
    +

    Создание чатов в VK Teams в случае аварий

    + Перейти +
    +
    +
    +
    + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..7039a26 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
    +
    +

    Login

    +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/logs.html b/templates/logs.html new file mode 100644 index 0000000..8c2335c --- /dev/null +++ b/templates/logs.html @@ -0,0 +1,10 @@ + +{% extends "base.html" %} +{% block title %}Логи{% endblock %} +{% block content %} +

    Логи

    +
    
    +{% endblock %}
    +{% block scripts %}
    +    
    +{% endblock %}
    \ No newline at end of file
    diff --git a/templates/regions.html b/templates/regions.html
    index 2642772..47be055 100644
    --- a/templates/regions.html
    +++ b/templates/regions.html
    @@ -1,53 +1,20 @@
    -
    -
    -
    -    
    -    
    -    Region Management
    -    
    -    
    -
    -
    -    
    -
    - -
    -

    Список Регионов

    -
    - -
    - - -
    - -
    -

    Пользователи

    -
    - -
    -
    - -
    -
      - -
    - - -
    - - - -
    -
    -
    -
    - - - + +{% extends "base.html" %} +{% block title %}Регионы{% endblock %} +{% block content %} +

    Регионы

    + + + + + + + + + + +
    IDНазваниеАктивен
    +{% endblock %} +{% block scripts %} - - +{% endblock %} \ No newline at end of file diff --git a/templates/users.html b/templates/users.html index 601bd1d..1a3a20f 100644 --- a/templates/users.html +++ b/templates/users.html @@ -1,37 +1,64 @@ - - - - - - Сотрудники - - - - - - - -
    -
    -
    -
      - -
    -
    -
    -
    -

    -
    Подписки на регионы
    -
      - -
    -
    Действия
    -
    - -
    +{% extends 'base.html' %} + +{% block content %} + + + Пользователи + +
    +

    Пользователи

    + + + + + + + + + + + + + + + + + +
    Chat IDTelegram IDEmailПодпискиТип уведомленийСтатусДействия
    + + + +
    + + + - - + + + + + +{% endblock %} diff --git a/templates/users_old.html b/templates/users_old.html new file mode 100644 index 0000000..601bd1d --- /dev/null +++ b/templates/users_old.html @@ -0,0 +1,37 @@ + + + + + + Сотрудники + + + + + + + +
    +
    +
    +
      + +
    +
    +
    +
    +

    +
    Подписки на регионы
    +
      + +
    +
    Действия
    +
    + +
    +
    +
    +
    +
    + + diff --git a/utilities/database.py b/utilities/database.py new file mode 100644 index 0000000..4c14ad6 --- /dev/null +++ b/utilities/database.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() # Создаем экземпляр SQLAlchemy \ No newline at end of file diff --git a/log_manager.py b/utilities/log_manager.py similarity index 98% rename from log_manager.py rename to utilities/log_manager.py index 908003b..03f7b71 100644 --- a/log_manager.py +++ b/utilities/log_manager.py @@ -62,12 +62,12 @@ class LogManager: }, 'handlers': { 'telebot_console': { - 'class': 'log_manager.UTF8StreamHandler', + 'class': 'utilities.log_manager.UTF8StreamHandler', 'stream': 'ext://sys.stdout', 'formatter': 'default', }, 'flask_console': { - 'class': 'log_manager.UTF8StreamHandler', + 'class': 'utilities.log_manager.UTF8StreamHandler', 'stream': 'ext://sys.stdout', 'formatter': 'werkzeug', }, diff --git a/rabbitmq.py b/utilities/rabbitmq.py similarity index 100% rename from rabbitmq.py rename to utilities/rabbitmq.py diff --git a/utils.py b/utilities/telegram_utilities.py similarity index 95% rename from utils.py rename to utilities/telegram_utilities.py index 33d19ba..5fe3b1e 100644 --- a/utils.py +++ b/utilities/telegram_utilities.py @@ -91,10 +91,10 @@ def escape_telegram_chars(text): def show_main_menu(chat_id): markup = telebot.types.ReplyKeyboardMarkup(one_time_keyboard=True, resize_keyboard=True) if bot_database.is_whitelisted(chat_id): - telezab.user_state_manager.set_state(chat_id, "MAIN_MENU") + telezab.state.set_state(chat_id, "MAIN_MENU") markup.add('Настройки', 'Помощь', 'Активные события') else: - telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + telezab.state.set_state(chat_id, "REGISTRATION") markup.add('Регистрация') backend_bot.bot.send_message(chat_id, "Выберите действие:", reply_markup=markup) @@ -109,7 +109,7 @@ def create_settings_keyboard(chat_id, admins_list): def show_settings_menu(chat_id): if not bot_database.is_whitelisted(chat_id): - telezab.user_state_manager.set_state(chat_id, "REGISTRATION") + telezab.state.set_state(chat_id, "REGISTRATION") backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота") return admins_list = bot_database.get_admins() diff --git a/user_state_manager.py b/utilities/user_state_manager.py similarity index 100% rename from user_state_manager.py rename to utilities/user_state_manager.py diff --git a/webui/__init__.py b/webui/__init__.py deleted file mode 100644 index 48a8f2b..0000000 --- a/webui/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from jinja2 import TemplateNotFound - -from flask import Blueprint, render_template, jsonify, abort - -webui = Blueprint('webui', __name__, url_prefix='/telezab') - - -@webui.route('/', defaults={'index'}) -def index(): - try: - return render_template(index.html) - except TemplateNotFound: - abort(404) - - -@webui.route('/data') -def get_data(): - return jsonify({"message": "Данные из frontend!"}) diff --git a/webui/index.py b/webui/index.py deleted file mode 100644 index 4982a05..0000000 --- a/webui/index.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Blueprint, jsonify - -webui = Blueprint('webui', __name__, url_prefix='/telezab') - -@webui.route("/heartbeat") -def heartbeat(): - return jsonify({"status": "healthy"}) - - -@webui.route('/', defaults={'path': ''}) -@webui.route('/') -def catch_all(): - return webui.send_static_file("index.html") \ No newline at end of file