Refactoring and cleanup codebase (50%...)

MVP dashboard users page
This commit is contained in:
Udo Chudo 2025-03-17 15:30:58 +05:00
parent 20465600b1
commit 9e2560f7c3
32 changed files with 937 additions and 423 deletions

View File

@ -9,3 +9,4 @@
/db/
/db/telezab.db
/trash/
/venv3.12.3/

1
.gitignore vendored
View File

@ -11,3 +11,4 @@
/logs/error.log
/db/
/db/telezab.db
/venv3.12.3/

View File

@ -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)

View File

@ -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'])

View File

@ -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):

View File

@ -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'

177
frontend/dashboard.py Normal file
View File

@ -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/<int:region_id>', 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/<int:user_id>', 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/<int:user_id>/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/<int:user_id>/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/<int:user_id>', 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'})

19
frontend/models.py Normal file
View File

@ -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())

14
frontend/routes/auth.py Normal file
View File

@ -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'))

9
frontend/routes/logs.py Normal file
View File

@ -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)

View File

@ -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)

9
frontend/routes/users.py Normal file
View File

@ -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)

View File

@ -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}"}

View File

@ -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

View File

@ -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;
}

View File

@ -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 = $('<li class="list-group-item"></li>').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 = `
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.subscriptions.join(', ') || 'Нет подписок'}</td>
<td>${user.disaster_only}</td>
<td>${user.status}</td>
<td><button class="btn btn-primary editUserBtn" data-id="${user.id}">Редактировать</button></td>
`;
tableBody.appendChild(row);
});
// Обработчик кликов по пользователям
$('#user-list').on('click', '.list-group-item', function() {
// Удаляем активный класс у всех элементов списка
$('.list-group-item').removeClass('active');
setupEditButtons();
}
// Добавляем активный класс к выбранному элементу
$(this).addClass('active');
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()));
// Отображаем регионы в одну строку
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('Нет подписок');
}
// Отображаем действия
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('<div><strong>' + event.date + '</strong> - ' + eventText + '</div>');
// Функция для обработки кнопок "Редактировать"
function setupEditButtons() {
document.querySelectorAll(".editUserBtn").forEach(button => {
button.addEventListener("click", function () {
const userId = this.dataset.id;
openUserModal(userId);
});
} else {
events.append('<div>Нет действий</div>');
}
});
}
// Функция открытия модального окна
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;
const blockBtn = document.getElementById("toggleBlockUser");
blockBtn.innerText = data.blocked ? "Разблокировать" : "Заблокировать";
blockBtn.onclick = function () {
toggleUserBlock(userId);
};
document.getElementById("viewUserEvents").onclick = function () {
viewUserEvents(userId);
};
document.getElementById("deleteUser").onclick = function () {
deleteUser(userId);
};
// Использование Bootstrap для показа модального окна
var myModal = new bootstrap.Modal(document.getElementById('userModal'), {
keyboard: false
});
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 = `<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(${currentPage - 1})">&laquo;</a>`;
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 = `<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(${currentPage + 1})">&raquo;</a>`;
paginationContainer.appendChild(nextButton);
}
// Запуск загрузки данных
document.addEventListener("DOMContentLoaded", () => {
loadUsers(currentPage);
});

View File

@ -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()

44
templates/base.html Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Dashboard{% endblock %}</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
</head>
<body>
<!-- Навигационное меню -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.dashboard' %}active{% endif %}" href="{{ url_for('dashboard.dashboard') }}">Главная</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.users_page' %}active{% endif %}" href="{{ url_for('dashboard.users_page') }}">Пользователи</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.regions_page' %}active{% endif %}" href="{{ url_for('dashboard.regions_page') }}">Регионы</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'dashboard.logs_page' %}active{% endif %}" href="{{ url_for('dashboard.logs_page') }}">Логи</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- Основной контент -->
<div class="container mt-4">
{% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
</body>
</html>

145
templates/dashboard.html Normal file
View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">Панель управления</h1>
<!-- Таблица пользователей -->
<h2>Пользователи</h2>
<table id="users-table" class="table table-bordered table-striped">
<thead class="thead-dark">
<tr>
<th>ID</th>
<th>Имя</th>
<th>Email</th>
<th>Подписки</th>
<th>Режим</th>
</tr>
</thead>
<tbody>
<!-- Данные загружаются через JS -->
</tbody>
</table>
<!-- Контейнер пагинации -->
<nav aria-label="Page navigation example">
<ul class="pagination" id="pagination">
<!-- Кнопка предыдущей страницы -->
<li class="page-item">
<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(currentPage - 1)">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>
</li>
<!-- Номера страниц (будет динамически генерироваться) -->
<!-- Пример -->
<!-- <li class="page-item"><a class="page-link" href="#">1</a></li> -->
<!-- Кнопка следующей страницы -->
<li class="page-item">
<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(currentPage + 1)">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
</nav>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
{#<script src="{{ url_for('static', filename='js/regions.js') }}"></script>#}
<script>
let currentPage = 1; // Переменная для текущей страницы
let totalPages = 1; // Переменная для общего количества страниц (инициализируем значением 1 по умолчанию)
const perPage = 20; // Количество элементов на странице
// Функция для загрузки данных
function loadUsers(page) {
if (page < 1 || page > totalPages) return; // Проверка на допустимые страницы
currentPage = page; // Обновляем текущую страницу
// Получаем данные с сервера
fetch(`/telezab/dashboard/users?page=${currentPage}&per_page=${perPage}`)
.then(response => response.json())
.then(data => {
totalPages = data.total_pages; // Обновляем totalPages из ответа сервера
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 = `
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.subscriptions.join(', ') || 'Нет подписок'}</td>
<td>${user.disaster_only}</td>
`;
tableBody.appendChild(row);
});
}
// Функция для обновления пагинации
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); // Отключить кнопку если текущая страница - 1
prevButton.innerHTML = `<a class="page-link" href="#" aria-label="Previous" onclick="loadUsers(${currentPage - 1})">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
</a>`;
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 = `<a class="page-link" href="#" aria-label="Next" onclick="loadUsers(${currentPage + 1})">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Next</span>
</a>`;
paginationContainer.appendChild(nextButton);
}
// Инициализация загрузки пользователей на первой странице
loadUsers(currentPage);
</script>
</body>
</html>

View File

@ -1,10 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index of WebUI for Telezab</title>
</head>
<body>
THIS IS WEBUI FOR TELEZAB BOT
</body>
</html>
{% extends "base.html" %}
{% block title %}Dashboard Home{% endblock %}
{% block content %}
<h1 class="text-center mb-4">Добро пожаловать в Dashboard</h1>
<p class="text-center">Выберите один из разделов:</p>
<div class="row">
<!-- Карточка пользователей -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Пользователи</h5>
<p class="card-text">Просмотр и управление пользователями.</p>
<a href="{{ url_for('dashboard.users_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка регионов -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Регионы</h5>
<p class="card-text">Просмотр и настройка регионов.</p>
<a href="{{ url_for('dashboard.regions_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка логов -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Логи</h5>
<p class="card-text">Просмотр событий и логов.</p>
<a href="{{ url_for('dashboard.logs_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
<!-- Карточка Teams Chat -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Создание чатов Teams</h5>
<p class="card-text">Создание чатов в VK Teams в случае аварий</p>
<a href="{{ url_for('dashboard.logs_page') }}" class="btn btn-primary">Перейти</a>
</div>
</div>
</div>
</div>
{% endblock %}

20
templates/login.html Normal file
View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<h2>Login</h2>
<form method="POST" action="{{ url_for('login') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
{% endblock %}

10
templates/logs.html Normal file
View File

@ -0,0 +1,10 @@
<!-- templates/logs.html -->
{% extends "base.html" %}
{% block title %}Логи{% endblock %}
{% block content %}
<h2>Логи</h2>
<pre id="logs-container"></pre>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/logs.js') }}"></script>
{% endblock %}

View File

@ -1,53 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Region Management</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/regions.css') }}">
</head>
<body>
<div class="container mt-4">
<div class="row">
<!-- Левый контейнер со списком регионов -->
<div class="col-md-6">
<h3>Список Регионов</h3>
<div class="list-group" id="region-list">
<!-- Список регионов будет добавляться сюда -->
</div>
<!-- Пагинация -->
<nav aria-label="Page navigation">
<ul class="pagination" id="pagination">
<!-- Кнопки пагинации будут добавляться сюда -->
</ul>
</nav>
</div>
<!-- Правый контейнер с информацией о пользователях -->
<div class="col-md-6">
<h3>Пользователи</h3>
<div id="curent-region">
</div>
<div id="user-info">
<!-- Информация о пользователях будет добавляться сюда -->
</div>
<ul id="user-list">
<!-- Список пользователей будет добавляться сюда -->
</ul>
<!-- Кнопки управления регионами -->
<div class="d-flex justify-content-end mt-3">
<button type="button" class="btn btn-danger me-2" id="delete-region">Удалить регион</button>
<button type="button" class="btn btn-warning me-2" id="disable-region">Отключить регион</button>
<button type="button" class="btn btn-success" id="activate-region">Активировать регион</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<!-- templates/regions.html -->
{% extends "base.html" %}
{% block title %}Регионы{% endblock %}
{% block content %}
<h2>Регионы</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Активен</th>
</tr>
</thead>
<tbody id="regions-table">
</tbody>
</table>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/regions.js') }}"></script>
</body>
</html>
{% endblock %}

View File

@ -1,37 +1,64 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сотрудники</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
{% extends 'base.html' %}
{% block content %}
<head>
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<title>Пользователи</title>
</head>
<div class="container">
<h1>Пользователи</h1>
<!-- Таблица пользователей -->
<table class="table" id="users-table">
<thead>
<tr>
<th>Chat ID</th>
<th>Telegram ID</th>
<th>Email</th>
<th>Подписки</th>
<th>Тип уведомлений</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<!-- Данные будут загружаться сюда динамически -->
</tbody>
</table>
<!-- Пагинация -->
<nav>
<ul class="pagination" id="pagination">
<!-- Страницы будут генерироваться динамически -->
</ul>
</nav>
</div>
<!-- Модальное окно -->
<div id="userModal" class="modal fade" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel">Редактирование пользователя</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Chat ID: <span id="userId"></span></p>
<p>Telegram ID: <span id="username"></span></p>
<p>Email: <span id="userEmail"></span></p>
</div>
<div class="modal-footer">
<button id="toggleBlockUser" class="btn btn-warning">Заблокировать</button>
<button id="viewUserEvents" class="btn btn-info">Посмотреть действия</button>
<button id="deleteUser" class="btn btn-danger">Удалить</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<!-- Подключаем внешний JavaScript файл -->
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<ul id="user-list" class="list-group">
<!-- Список сотрудников будет вставлен сюда -->
</ul>
</div>
<div class="col-md-8">
<div id="user-info" class="d-none">
<h3 id="user-name"></h3>
<h5>Подписки на регионы</h5>
<ul id="user-regions" class="list-group">
<!-- Подписки на регионы будут вставлены сюда -->
</ul>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
<!-- Действия будут вставлены сюда -->
</div>
</div>
</div>
</div>
</div>
</body>
</html>
{% endblock %}

37
templates/users_old.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Сотрудники</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
<script src="{{ url_for('static', filename='js/jquery-3.6.0.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-md-4">
<ul id="user-list" class="list-group">
<!-- Список сотрудников будет вставлен сюда -->
</ul>
</div>
<div class="col-md-8">
<div id="user-info" class="d-none">
<h3 id="user-name"></h3>
<h5>Подписки на регионы</h5>
<ul id="user-regions" class="list-group">
<!-- Подписки на регионы будут вставлены сюда -->
</ul>
<h5>Действия</h5>
<div id="user-events" class="overflow-auto" style="max-height: 300px;">
<!-- Действия будут вставлены сюда -->
</div>
</div>
</div>
</div>
</div>
</body>
</html>

3
utilities/database.py Normal file
View File

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # Создаем экземпляр SQLAlchemy

View File

@ -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',
},

View File

@ -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()

View File

@ -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!"})

View File

@ -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('/<path:path>')
def catch_all():
return webui.send_static_file("index.html")