Говорила мне мама...ну нахер эти ваши HDD
18
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM python:3.12.3-slim
|
||||
FROM python:3.13.1-slim
|
||||
LABEL authors="UdoChudo"
|
||||
# Установим необходимые пакеты
|
||||
RUN apt-get update && apt-get install -y \
|
||||
@ -9,23 +9,29 @@ RUN apt-get update && apt-get install -y \
|
||||
sqlite3 \
|
||||
curl \
|
||||
telnet \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# Установим рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Скопируем файлы проекта
|
||||
COPY . /app
|
||||
|
||||
# Установим зависимости проекта
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Копируем конфигурацию supervisord
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Установим зависимости проекта
|
||||
RUN mkdir -p /app/logs
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir gunicorn==23.0.0
|
||||
# Откроем порт для нашего приложения
|
||||
EXPOSE 5000
|
||||
ENV TZ=Europe/Moscow
|
||||
ENV FLASK_APP telezab.py
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Запуск Gunicorn
|
||||
CMD ["python3", "telezab.py"]
|
||||
|
||||
|
||||
# Указываем команду для запуска supervisord
|
||||
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
262
backend/api.py
Normal file
@ -0,0 +1,262 @@
|
||||
from flask import jsonify, request, Blueprint
|
||||
from flask_login import login_required
|
||||
|
||||
from frontend.dashboard import user_manager, event_manager, region_manager, system_manager
|
||||
from utilities.database import db
|
||||
from utilities.web_logger import WebLogger
|
||||
|
||||
bp_api = Blueprint('api', __name__, url_prefix='/telezab/rest/api')
|
||||
web_logger = WebLogger(db)
|
||||
|
||||
|
||||
@bp_api.route('/users', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_users():
|
||||
if request.method == 'GET':
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
return jsonify(user_manager.get_users(page, per_page))
|
||||
elif request.method == 'POST':
|
||||
user_data = request.get_json()
|
||||
try:
|
||||
result, status_code = user_manager.add_user(user_data)
|
||||
if status_code == 201:
|
||||
web_logger.log_web_action(
|
||||
action='Добавление пользователя Telegram',
|
||||
details=f'Telegram ID: {user_data.get("chat_id")}, Username: {user_data.get("username")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_user(chat_id):
|
||||
user = user_manager.get_user(chat_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'Пользователь не найден'}), 404
|
||||
return jsonify(user)
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>/block', methods=['POST'])
|
||||
@login_required
|
||||
def block_user(chat_id):
|
||||
user_info = user_manager.get_user(chat_id)
|
||||
blocked = user_manager.toggle_block_user(chat_id)
|
||||
if blocked is not None:
|
||||
status = 'заблокирован' if blocked else 'разблокирован'
|
||||
web_logger.log_web_action(
|
||||
action=f'Блокировка/разблокировка пользователя Telegram',
|
||||
details=f'Telegram ID: {chat_id}, Username: {user_info.get("username") if user_info else "неизвестно"}, Статус: {status}'
|
||||
)
|
||||
return jsonify({'status': 'updated', 'new_status': blocked})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_user(chat_id):
|
||||
user_info = user_manager.get_user(chat_id)
|
||||
if user_manager.delete_user(chat_id):
|
||||
web_logger.log_web_action(
|
||||
action='Удаление пользователя Telegram',
|
||||
details=f'Telegram ID: {chat_id}, Username: {user_info.get("username") if user_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify({'status': 'deleted'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'User not found'}), 404
|
||||
|
||||
|
||||
@bp_api.route('/users/<int:chat_id>/log', methods=['POST'])
|
||||
@login_required
|
||||
def log_user_action(chat_id):
|
||||
action = request.json.get('action')
|
||||
if action:
|
||||
event_manager.log_user_action(chat_id, action)
|
||||
return jsonify({'message': 'Действие сохранено'}), 200
|
||||
else:
|
||||
return jsonify({'error': 'Не указано действие'}), 400
|
||||
|
||||
@bp_api.route('/users/search', methods=['GET'])
|
||||
@login_required
|
||||
def search_users():
|
||||
telegram_id = request.args.get('telegram_id')
|
||||
email = request.args.get('email')
|
||||
users = user_manager.search_users(telegram_id, email)
|
||||
return jsonify(users)
|
||||
|
||||
@bp_api.route('/user_events/<int:chat_id>', methods=['GET'])
|
||||
@login_required
|
||||
def handle_user_events(chat_id):
|
||||
return event_manager.get_user_events(chat_id)
|
||||
|
||||
|
||||
|
||||
@bp_api.route('/regions', methods=['GET', 'POST', 'PUT', 'DELETE'])
|
||||
@login_required
|
||||
def manage_regions():
|
||||
if request.method == 'POST':
|
||||
region_data = request.get_json()
|
||||
result = region_manager.add_region(region_data)
|
||||
web_logger.log_web_action(
|
||||
action='Добавление региона',
|
||||
details=f'Название: {region_data.get("name")}, Номер: {region_data.get("number")}'
|
||||
)
|
||||
return jsonify(result)
|
||||
elif request.method == 'PUT':
|
||||
region_data = request.get_json()
|
||||
if 'active' in region_data:
|
||||
result = region_manager.update_region_status(region_data)
|
||||
status = 'активирован' if region_data.get('active') else 'деактивирован'
|
||||
web_logger.log_web_action(
|
||||
action='Изменение статуса региона',
|
||||
details=f'ID: {region_data.get("region_id")}, Статус: {status}'
|
||||
)
|
||||
return jsonify(result)
|
||||
elif 'name' in region_data:
|
||||
result = region_manager.update_region_name(region_data)
|
||||
web_logger.log_web_action(
|
||||
action='Изменение названия региона',
|
||||
details=f'ID: {region_data.get("region_id")}, Новое название: {region_data.get("name")}'
|
||||
)
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Некорректный запрос'}), 400
|
||||
elif request.method == 'DELETE':
|
||||
region_id = request.args.get('region_id')
|
||||
region_info = region_manager.get_region(region_id)
|
||||
result = region_manager.delete_region(region_id)
|
||||
if result.get('status') == 'success':
|
||||
web_logger.log_web_action(
|
||||
action='Удаление региона',
|
||||
details=f'ID: {region_id}, Название: {region_info.get("region_name") if region_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify(result)
|
||||
|
||||
@bp_api.route('/regions/<region_id>/subscribers', methods=['GET'])
|
||||
@login_required
|
||||
def get_region_subscribers(region_id):
|
||||
result, status_code = region_manager.get_region_subscribers(region_id)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/systems', methods=['GET'])
|
||||
@login_required
|
||||
def get_systems():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
sort_field = request.args.get('sort_field', 'system_id')
|
||||
sort_order = request.args.get('sort_order', 'asc')
|
||||
|
||||
result = system_manager.get_systems(page, per_page, sort_field, sort_order)
|
||||
return jsonify(result)
|
||||
|
||||
@bp_api.route('/systems', methods=['POST', 'PUT', 'DELETE'])
|
||||
@login_required
|
||||
def manage_systems():
|
||||
if request.method == 'POST':
|
||||
data = request.get_json()
|
||||
result, status_code = system_manager.add_system(data)
|
||||
if status_code == 201:
|
||||
web_logger.log_web_action(
|
||||
action='Добавление системы',
|
||||
details=f'ID: {data.get("system_id")}, Название: {data.get("name")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
elif request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
system_info_before = system_manager.get_system(data.get('system_id'))
|
||||
result, status_code = system_manager.update_system_name(data)
|
||||
if status_code == 200:
|
||||
web_logger.log_web_action(
|
||||
action='Изменение названия системы',
|
||||
details=f'ID: {data.get("system_id")}, Старое название: {system_info_before.get("name") if system_info_before else "неизвестно"}, Новое название: {data.get("name")}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
elif request.method == 'DELETE':
|
||||
system_id = request.args.get('system_id')
|
||||
system_info = system_manager.get_system(system_id)
|
||||
result, status_code = system_manager.delete_system(system_id)
|
||||
if status_code == 200:
|
||||
web_logger.log_web_action(
|
||||
action='Удаление системы',
|
||||
details=f'ID: {system_id}, Название: {system_info.get("name") if system_info else "неизвестно"}'
|
||||
)
|
||||
return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/web_logs', methods=['GET'])
|
||||
@login_required
|
||||
def get_web_logs():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
ldap_user_id_filter = request.args.get('user_id', None, type=str)
|
||||
action_filter = request.args.get('action', None, type=str)
|
||||
|
||||
logs_data = web_logger.get_web_action_logs(page, per_page, ldap_user_id_filter, action_filter)
|
||||
return jsonify(logs_data)
|
||||
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['POST'])
|
||||
# @login_required
|
||||
# def add_system():
|
||||
# data = request.get_json()
|
||||
# result, status_code = system_manager.add_system(data)
|
||||
# return jsonify(result), status_code
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['PUT'])
|
||||
# @login_required
|
||||
# def update_system():
|
||||
# data = request.get_json()
|
||||
# result, status_code = system_manager.update_system_name(data)
|
||||
# return jsonify(result), status_code
|
||||
#
|
||||
# @bp_api.route('/systems', methods=['DELETE'])
|
||||
# @login_required
|
||||
# def delete_system():
|
||||
# system_id = request.args.get('system_id')
|
||||
# result, status_code = system_manager.delete_system(system_id)
|
||||
# return jsonify(result), status_code
|
||||
|
||||
@bp_api.route('/debug/log-level', methods=['POST'])
|
||||
@login_required
|
||||
def set_log_level():
|
||||
from telezab import log_manager
|
||||
try:
|
||||
data = request.get_json()
|
||||
component = data.get('component').lower()
|
||||
level = data.get('level').upper()
|
||||
success, message = log_manager.change_log_level(component, level)
|
||||
if success:
|
||||
return jsonify({'status': 'success', 'message': message}), 200
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': message}), 400
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@bp_api.route('/notifications', methods=['POST'])
|
||||
def notification():
|
||||
from utilities.notification_manager import NotificationManager
|
||||
from utilities.telegram_utilities import extract_region_number, format_message
|
||||
from backend_flask import app
|
||||
try:
|
||||
data = request.get_json()
|
||||
app.logger.info(f"Получены данные уведомления: {data}")
|
||||
region_id = extract_region_number(data.get("host"))
|
||||
if region_id is None:
|
||||
app.logger.error(f"Не удалось извлечь номер региона из host: {data.get('host')}")
|
||||
return jsonify({"status": "error", "message": "Invalid host format"}), 400
|
||||
app.logger.debug(f"Извлечён номер региона: {region_id}")
|
||||
|
||||
manager = NotificationManager(app.logger)
|
||||
subscribers = manager.get_subscribers(region_id, data['severity'])
|
||||
if manager.is_region_active(region_id):
|
||||
message = format_message(data)
|
||||
manager.send_notifications(subscribers, message)
|
||||
|
||||
return jsonify({"status": "success"}), 200
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Ошибка при обработке уведомления: {e}")
|
||||
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500
|
||||
129
backend/auth.py
Normal file
@ -0,0 +1,129 @@
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, current_app
|
||||
from flask_ldap3_login import LDAP3LoginManager, AuthenticationResponseStatus
|
||||
from flask_login import LoginManager, login_user, UserMixin, logout_user, current_user
|
||||
from datetime import timedelta
|
||||
|
||||
import config
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
bp_auth = Blueprint('auth', __name__, url_prefix='/telezab/')
|
||||
|
||||
login_manager = LoginManager()
|
||||
logging.getLogger('flask-login').setLevel(logging.DEBUG)
|
||||
logging.getLogger('flask_ldap3_login').setLevel(logging.DEBUG)
|
||||
logging.getLogger('ldap3').setLevel(logging.DEBUG)
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, user_id, user_name=None, user_surname=None, user_middle_name=None,display_name=None, email=None):
|
||||
self.id = str(user_id)
|
||||
self.user_name = user_name
|
||||
self.user_surname = user_surname
|
||||
self.user_middle_name = user_middle_name
|
||||
self.display_name = display_name
|
||||
self.email = email
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
logging.debug(f"load_user called for user_id: {user_id}")
|
||||
display_name = session.get('display_name') # Получаем display_name из сессии
|
||||
return User(user_id, display_name=display_name)
|
||||
|
||||
@bp_auth.record_once
|
||||
def on_load(state):
|
||||
login_manager.init_app(state.app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
init_ldap(state.app)
|
||||
|
||||
|
||||
def init_ldap(app):
|
||||
app.config['LDAP_HOST'] = config.LDAP_HOST
|
||||
app.config['LDAP_PORT'] = config.LDAP_PORT
|
||||
app.config['LDAP_USE_SSL'] = config.LDAP_USE_SSL
|
||||
app.config['LDAP_BASE_DN'] = config.LDAP_BASE_DN
|
||||
app.config['LDAP_BIND_DIRECT_CREDENTIALS'] = False
|
||||
app.config['LDAP_BIND_USER_DN'] = config.LDAP_BIND_USER_DN
|
||||
app.config['LDAP_BIND_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_DN'] = config.LDAP_USER_DN
|
||||
app.config['LDAP_USER_PASSWORD'] = config.LDAP_USER_PASSWORD
|
||||
app.config['LDAP_USER_OBJECT_FILTER'] = config.LDAP_USER_OBJECT_FILTER
|
||||
app.config['LDAP_USER_LOGIN_ATTR'] = config.LDAP_USER_LOGIN_ATTR
|
||||
app.config['LDAP_USER_SEARCH_SCOPE'] = config.LDAP_USER_SEARCH_SCOPE
|
||||
app.config['LDAP_SCHEMA'] = config.LDAP_SCHEMA
|
||||
|
||||
ldap_manager = LDAP3LoginManager(app)
|
||||
app.extensions['ldap3_login'] = ldap_manager
|
||||
ldap_manager.init_app(app)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1)
|
||||
|
||||
def get_attr(user_info, attr_name):
|
||||
try:
|
||||
value = user_info.get(attr_name)
|
||||
if isinstance(value, list) and value:
|
||||
return str(value[0])
|
||||
elif value:
|
||||
return str(value)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting attribute {attr_name}: {e}")
|
||||
return None
|
||||
|
||||
@bp_auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if 'user_id' in session:
|
||||
return redirect(url_for('dashboard.dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
ldap_manager = current_app.extensions['ldap3_login']
|
||||
|
||||
try:
|
||||
ldap_response = ldap_manager.authenticate(username, password)
|
||||
logging.debug(f"ldap_response.status: {ldap_response.status}")
|
||||
|
||||
if ldap_response.status == AuthenticationResponseStatus.success:
|
||||
user_info = ldap_response.user_info
|
||||
logging.debug(f"user_info: {user_info}")
|
||||
|
||||
if not user_info:
|
||||
logging.error("LDAP authentication succeeded but no user info was returned.")
|
||||
flash("Failed to retrieve user details from LDAP.", "danger")
|
||||
return render_template("login.html")
|
||||
|
||||
sam_account_name = get_attr(user_info, "sAMAccountName")
|
||||
# display_name = get_attr(user_info, "displayName")
|
||||
email = get_attr(user_info, "mail")
|
||||
user_name = get_attr(user_info, "givenName")
|
||||
user_middle_name = get_attr(user_info, "middleName")
|
||||
user_surname = get_attr(user_info, "sn")
|
||||
display_name = f"{user_surname} {user_name} {user_middle_name}"
|
||||
user = User(user_id=sam_account_name,
|
||||
user_name=user_name,
|
||||
user_surname=user_surname,
|
||||
user_middle_name=user_middle_name,
|
||||
display_name=display_name,
|
||||
email=email
|
||||
)
|
||||
|
||||
session.permanent = True
|
||||
session['username'] = sam_account_name
|
||||
session['display_name'] = display_name # Сохраняем display_name в сессии
|
||||
login_user(user)
|
||||
logging.debug(f"current_user: {current_user.__dict__}")
|
||||
logging.info(f"User {user.id} logged in successfully.")
|
||||
# log_user_action(action='Успешная авторизация', details=f'Username: {username}') # Логируем успешную авторизацию
|
||||
flash("Logged in successfully!", "success")
|
||||
return redirect(url_for("dashboard.dashboard"))
|
||||
|
||||
elif ldap_response.status == AuthenticationResponseStatus.fail:
|
||||
flash('Invalid username or password.', 'danger')
|
||||
else:
|
||||
flash(f"LDAP Error: {ldap_response.status}", 'danger')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error during login: {e}")
|
||||
flash("An unexpected error occurred. Please try again.", 'danger')
|
||||
|
||||
return render_template('login.html')
|
||||
@ -5,8 +5,8 @@ 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 utilities.telegram_utilities import show_main_menu, show_settings_menu
|
||||
from handlers import handle_my_subscriptions_button, handle_active_regions_button, handle_notification_mode_button
|
||||
|
||||
|
||||
def handle_main_menu(message, chat_id, text):
|
||||
|
||||
310
backend_flask.py
@ -1,54 +1,69 @@
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
import telebot
|
||||
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 flask import Flask, request, jsonify, redirect, url_for
|
||||
from flask_login import LoginManager
|
||||
|
||||
from frontend.dashboard import bp_dashboard, bp_api
|
||||
|
||||
import backend_bot
|
||||
import bot_database
|
||||
import telezab
|
||||
import utilities.telegram_utilities as telegram_util
|
||||
import config
|
||||
from frontend.dashboard import bp_dashboard
|
||||
from backend.api import bp_api
|
||||
from backend.auth import bp_auth, User
|
||||
from backend_locks import db_lock
|
||||
from config import BASE_URL, DB_PATH
|
||||
from utilities.telegram_utilities import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email
|
||||
from config import DB_PATH, TZ
|
||||
from utilities.database import db
|
||||
from utilities.telegram_utilities import extract_region_number, format_message
|
||||
|
||||
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"
|
||||
login_manager = LoginManager()
|
||||
|
||||
# Пользовательский класс
|
||||
class User(UserMixin):
|
||||
def __init__(self, dn, username):
|
||||
self.id = dn
|
||||
self.username = username
|
||||
def create_app():
|
||||
app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
|
||||
app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
|
||||
app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY # Убедитесь, что установлено значение True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = False
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME
|
||||
app.config['SESSION_COOKIE_MAX_AGE'] = 3600
|
||||
app.config['TIMEZONE'] = TZ
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def unauthorized():
|
||||
logging.debug("Unauthorized access detected")
|
||||
if request.path.startswith('/telezab/rest/api'):
|
||||
return jsonify({'error': 'Не авторизован'}), 401
|
||||
else:
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
app.register_blueprint(bp_dashboard)
|
||||
app.register_blueprint(bp_auth)
|
||||
app.register_blueprint(bp_api)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
login_manager.init_app(app) # Инициализация login_manager
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User(user_id)
|
||||
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
# Настройка уровня логирования для Flask
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
@app.route(BASE_URL + '/webhook', methods=['POST'])
|
||||
@app.route('/telezab/webhook', methods=['POST'])
|
||||
def webhook():
|
||||
try:
|
||||
# Получаем данные и логируем
|
||||
data = request.get_json()
|
||||
app.logger.info(f"Получены данные: {data}")
|
||||
|
||||
# # Генерация хеша события и логирование
|
||||
# event_hash = bot_database.hash_data(data)
|
||||
# app.logger.debug(f"Сгенерирован хеш для события: {event_hash}")
|
||||
|
||||
# Работа с базой данных в блоке синхронизации
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
@ -135,228 +150,3 @@ def webhook():
|
||||
app.logger.error(f"Неожиданная ошибка: {e}")
|
||||
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500
|
||||
|
||||
|
||||
@app.route(BASE_URL + '/users/add', methods=['POST'])
|
||||
def add_user():
|
||||
data = request.get_json()
|
||||
|
||||
telegram_id = data.get('telegram_id')
|
||||
chat_id = data.get('chat_id')
|
||||
user_email = data.get('user_email')
|
||||
|
||||
# DEBUG: Логирование полученных данных
|
||||
app.logger.debug(f"Получены данные для добавления пользователя: {data}")
|
||||
|
||||
# Валидация данных
|
||||
if not validate_chat_id(chat_id):
|
||||
app.logger.warning(f"Ошибка валидации: некорректный chat_id: {chat_id}")
|
||||
return jsonify({"status": "failure", "reason": "Invalid data chat_id must be digit"}), 400
|
||||
|
||||
if not validate_telegram_id(telegram_id):
|
||||
app.logger.warning(f"Ошибка валидации: некорректный telegram_id: {telegram_id}")
|
||||
return jsonify({"status": "failure", "reason": "Invalid data telegram id must start from '@'"}), 400
|
||||
|
||||
if not validate_email(user_email):
|
||||
app.logger.warning(f"Ошибка валидации: некорректный email: {user_email}")
|
||||
return jsonify({"status": "failure", "reason": "Invalid data email address must be from rtmis"}), 400
|
||||
|
||||
if telegram_id and chat_id and user_email:
|
||||
try:
|
||||
# INFO: Попытка отправить сообщение пользователю
|
||||
app.logger.info(f"Отправка сообщения пользователю {telegram_id} с chat_id {chat_id}")
|
||||
backend_bot.bot.send_message(chat_id, "Регистрация пройдена успешно.")
|
||||
# DEBUG: Попытка добавления пользователя в whitelist
|
||||
app.logger.debug(f"Добавление пользователя {telegram_id} в whitelist")
|
||||
success = bot_database.rundeck_add_to_whitelist(chat_id, telegram_id, user_email)
|
||||
if success:
|
||||
# INFO: Пользователь успешно добавлен в whitelist
|
||||
app.logger.info(f"Пользователь {telegram_id} добавлен в whitelist.")
|
||||
telezab.state.set_state(chat_id, "MAIN_MENU")
|
||||
|
||||
# DEBUG: Показ основного меню пользователю
|
||||
app.logger.debug(f"Отображение основного меню для пользователя с chat_id {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:
|
||||
# INFO: Пользователь уже существует в системе
|
||||
app.logger.info(f"Пользователь с chat_id {chat_id} уже существует.")
|
||||
return jsonify({"status": "failure", "msg": "User already exists"}), 400
|
||||
except telebot.apihelper.ApiTelegramException as e:
|
||||
if e.result.status_code == 403:
|
||||
# INFO: Пользователь заблокировал бота
|
||||
app.logger.info(f"Пользователь {telegram_id} заблокировал бота")
|
||||
return jsonify({"status": "failure", "msg": f"User {telegram_id} is blocked chat with bot"})
|
||||
elif e.result.status_code == 400:
|
||||
# WARNING: Пользователь неизвестен боту, возможно не нажал /start
|
||||
app.logger.warning(
|
||||
f"Пользователь {telegram_id} с chat_id {chat_id} неизвестен боту, возможно, не нажал /start")
|
||||
return jsonify({"status": "failure",
|
||||
"msg": f"User {telegram_id} with {chat_id} is unknown to the bot, did the user press /start button?"})
|
||||
else:
|
||||
# ERROR: Неизвестная ошибка при отправке сообщения
|
||||
app.logger.error(f"Ошибка при отправке сообщения пользователю {telegram_id}: {str(e)}")
|
||||
return jsonify({"status": "failure", "msg": f"{e}"})
|
||||
else:
|
||||
# ERROR: Ошибка валидации — недостаточно данных
|
||||
app.logger.error("Получены некорректные данные для добавления пользователя.")
|
||||
return jsonify({"status": "failure", "reason": "Invalid data"}), 400
|
||||
|
||||
|
||||
@app.route(BASE_URL + '/users/del', methods=['POST'])
|
||||
def delete_user():
|
||||
data = request.get_json()
|
||||
user_email = data.get('email')
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
# DEBUG: Получен запрос и начинается обработка
|
||||
app.logger.debug(f"Получен запрос на удаление пользователя. Данные: {data}")
|
||||
|
||||
if not user_email:
|
||||
# WARNING: Ошибка валидации данных, email отсутствует
|
||||
app.logger.warning(f"Ошибка валидации: отсутствует email")
|
||||
return jsonify({"status": "failure", "message": "Email is required"}), 400
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# DEBUG: Запрос на получение chat_id
|
||||
app.logger.debug(f"Выполняется запрос на получение chat_id для email: {user_email}")
|
||||
cursor.execute("SELECT chat_id FROM whitelist WHERE user_email = ?", (user_email,))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if user is None:
|
||||
# WARNING: Пользователь с указанным email не найден
|
||||
app.logger.warning(f"Пользователь с email {user_email} не найден")
|
||||
return jsonify({"status": "failure", "message": "User not found"}), 404
|
||||
chat_id = user[0]
|
||||
|
||||
# INFO: Удаление пользователя и его подписок начато
|
||||
app.logger.info(f"Начато удаление пользователя с email {user_email} и всех его подписок")
|
||||
|
||||
# DEBUG: Удаление пользователя из whitelist
|
||||
app.logger.debug(f"Удаление пользователя с email {user_email} из whitelist")
|
||||
cursor.execute("DELETE FROM whitelist WHERE user_email = ?", (user_email,))
|
||||
|
||||
# DEBUG: Удаление подписок пользователя
|
||||
app.logger.debug(f"Удаление подписок для пользователя с chat_id {chat_id}")
|
||||
cursor.execute("DELETE FROM subscriptions WHERE chat_id = ?", (chat_id,))
|
||||
|
||||
conn.commit()
|
||||
# INFO: Пользователь и подписки успешно удалены
|
||||
app.logger.info(f"Пользователь с email {user_email} и все его подписки успешно удалены")
|
||||
return jsonify(
|
||||
{"status": "success", "message": f"User with email {user_email} and all subscriptions deleted."}), 200
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
# ERROR: Ошибка при удалении данных
|
||||
app.logger.error(f"Ошибка при удалении пользователя с email {user_email}: {str(e)}")
|
||||
return jsonify({"status": "failure", "message": str(e)}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
# DEBUG: Соединение с базой данных закрыто
|
||||
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 + '/debug/flask', methods=['POST'])
|
||||
def toggle_flask_debug():
|
||||
try:
|
||||
data = request.get_json()
|
||||
level = data.get('level').upper()
|
||||
if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400
|
||||
|
||||
log_level = getattr(logging, level, logging.DEBUG)
|
||||
app.logger.setLevel(log_level)
|
||||
|
||||
for handler in app.logger.handlers:
|
||||
handler.setLevel(log_level)
|
||||
|
||||
return jsonify({'status': 'success', 'level': level})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route(BASE_URL + '/debug/telebot', methods=['POST'])
|
||||
def toggle_telebot_debug():
|
||||
try:
|
||||
data = request.get_json()
|
||||
level = data.get('level').upper()
|
||||
if level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
return jsonify({'status': 'error', 'message': 'Invalid log level'}), 400
|
||||
|
||||
log_level = getattr(logging, level, logging.DEBUG)
|
||||
telebot.logger.setLevel(log_level)
|
||||
|
||||
for handler in telebot.logger.handlers:
|
||||
handler.setLevel(log_level)
|
||||
|
||||
return jsonify({'status': 'success', 'level': level})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
@ -1,23 +1,35 @@
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import telebot
|
||||
from pytz import timezone
|
||||
from pyzabbix import ZabbixAPI
|
||||
from pyzabbix import ZabbixAPI, ZabbixAPIException
|
||||
|
||||
import backend_bot
|
||||
from config import ZABBIX_URL, ZABBIX_API_TOKEN
|
||||
from utilities.telegram_utilities import show_main_menu
|
||||
from utilities.telegram_utilities import show_main_menu, escape_telegram_chars
|
||||
|
||||
zabbix_logger = logging.getLogger("pyzabbix")
|
||||
|
||||
|
||||
def get_triggers_for_group(chat_id, group_id):
|
||||
triggers = get_zabbix_triggers(group_id) # Получаем все активные события без периода
|
||||
try:
|
||||
triggers = get_zabbix_triggers(group_id)
|
||||
if not triggers:
|
||||
backend_bot.bot.send_message(chat_id, f"Нет активных событий.")
|
||||
backend_bot.bot.send_message(chat_id, "Нет активных событий.")
|
||||
zabbix_logger.debug(f"No active triggers found for group {group_id}.")
|
||||
show_main_menu(chat_id)
|
||||
else:
|
||||
send_triggers_to_user(triggers, chat_id)
|
||||
zabbix_logger.debug(f"Sent {len(triggers)} triggers to user {chat_id} for group {group_id}.")
|
||||
except ZabbixAPIException as e:
|
||||
zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка Zabbix API.")
|
||||
show_main_menu(chat_id)
|
||||
except Exception as e:
|
||||
zabbix_logger.error(f"Error getting triggers for group {group_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка при получении событий.")
|
||||
show_main_menu(chat_id)
|
||||
|
||||
|
||||
def get_triggers_for_all_groups(chat_id, region_id):
|
||||
@ -30,17 +42,31 @@ def get_triggers_for_all_groups(chat_id, region_id):
|
||||
|
||||
all_triggers = []
|
||||
for group in filtered_groups:
|
||||
try:
|
||||
triggers = get_zabbix_triggers(group['groupid'])
|
||||
if triggers:
|
||||
all_triggers.extend(triggers)
|
||||
except ZabbixAPIException as e:
|
||||
zabbix_logger.error(f"Zabbix API error for group {group['groupid']} ({group['name']}): {e}")
|
||||
backend_bot.bot.send_message(chat_id, f"Ошибка Zabbix API при получении событий для группы {group['name']}.")
|
||||
except Exception as e:
|
||||
zabbix_logger.error(f"Error getting triggers for group {group['groupid']} ({group['name']}): {e}")
|
||||
backend_bot.bot.send_message(chat_id, f"Ошибка при получении событий для группы {group['name']}.")
|
||||
|
||||
if all_triggers:
|
||||
send_triggers_to_user(all_triggers, chat_id)
|
||||
zabbix_logger.debug(f"Sent {len(all_triggers)} triggers to user {chat_id} for region {region_id}.")
|
||||
else:
|
||||
backend_bot.bot.send_message(chat_id, f"Нет активных событий.")
|
||||
backend_bot.bot.send_message(chat_id, "Нет активных событий.")
|
||||
zabbix_logger.debug(f"No active triggers found for region {region_id}.")
|
||||
show_main_menu(chat_id)
|
||||
except ZabbixAPIException as e:
|
||||
zabbix_logger.error(f"Zabbix API error for region {region_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка Zabbix API.")
|
||||
show_main_menu(chat_id)
|
||||
except Exception as e:
|
||||
backend_bot.bot.send_message(chat_id, f"Ошибка при получении событий.\n{str(e)}")
|
||||
zabbix_logger.error(f"Error getting triggers for region {region_id}: {e}")
|
||||
backend_bot.bot.send_message(chat_id, "Ошибка при получении событий.")
|
||||
show_main_menu(chat_id)
|
||||
|
||||
|
||||
@ -56,73 +82,86 @@ def extract_host_from_name(name):
|
||||
|
||||
|
||||
def get_zabbix_triggers(group_id):
|
||||
pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"}
|
||||
start_time = time.time()
|
||||
try:
|
||||
zapi = ZabbixAPI(ZABBIX_URL)
|
||||
zapi.login(api_token=ZABBIX_API_TOKEN)
|
||||
telebot.logger.info(f"Fetching active hosts for group {group_id}")
|
||||
|
||||
# Получаем список активных хостов в группе
|
||||
active_hosts = zapi.host.get(
|
||||
groupids=group_id,
|
||||
output=["hostid", "name"],
|
||||
filter={"status": "0"} # Только включенные хосты
|
||||
)
|
||||
|
||||
if not active_hosts:
|
||||
telebot.logger.info(f"No active hosts found for group {group_id}")
|
||||
return []
|
||||
|
||||
host_ids = [host["hostid"] for host in active_hosts]
|
||||
telebot.logger.info(f"Found {len(host_ids)} active hosts in group {group_id}")
|
||||
|
||||
# Получение активных проблем для этих хостов
|
||||
problems = zapi.problem.get(
|
||||
output=["eventid", "name", "severity", "clock"],
|
||||
hostids=host_ids,
|
||||
severities=[4, 5],
|
||||
suppressed=0,
|
||||
acknowledged=0,
|
||||
filter={"severity": ["4", "5"]}, # Только высокий и аварийный уровень
|
||||
sortorder="ASC"
|
||||
groupids=group_id
|
||||
)
|
||||
trigger_ids = [problem["objectid"] for problem in problems]
|
||||
|
||||
triggers = zapi.trigger.get(
|
||||
triggerids=trigger_ids,
|
||||
output=["triggerid", "description", "priority"],
|
||||
selectHosts=["hostid", "name"],
|
||||
monitored=1,
|
||||
expandDescription=1,
|
||||
expandComment=1,
|
||||
selectItems=["itemid", "lastvalue"],
|
||||
selectLastEvent=["clock", "eventid"]
|
||||
)
|
||||
|
||||
if not problems:
|
||||
telebot.logger.info(f"No active problems found for group {group_id}")
|
||||
return []
|
||||
|
||||
# Получение IP-адресов хостов
|
||||
host_interfaces = zapi.hostinterface.get(
|
||||
hostids=host_ids,
|
||||
output=["hostid", "ip"]
|
||||
events = zapi.event.get(
|
||||
severities=[4, 5],
|
||||
objectids=trigger_ids,
|
||||
select_alerts="mediatype"
|
||||
)
|
||||
host_ip_map = {iface["hostid"]: iface["ip"] for iface in host_interfaces}
|
||||
# print(host_ip_map)
|
||||
moscow_tz = timezone('Europe/Moscow')
|
||||
severity_map = {'4': 'HIGH', '5': 'DISASTER'}
|
||||
priority_map = {'4': '⚠️', '5': '⛔️'}
|
||||
problem_messages = []
|
||||
|
||||
for problem in problems:
|
||||
event_time_epoch = int(problem['clock'])
|
||||
event_time = datetime.fromtimestamp(event_time_epoch, tz=moscow_tz)
|
||||
pnet_triggers = []
|
||||
event_dict = {event["objectid"]: event for event in events}
|
||||
|
||||
for trigger in triggers:
|
||||
event = event_dict.get(trigger["triggerid"])
|
||||
if event:
|
||||
for alert in event["alerts"]:
|
||||
if alert["mediatypes"] and alert["mediatypes"][0]["name"] in pnet_mediatypes and trigger not in pnet_triggers:
|
||||
pnet_triggers.append(trigger)
|
||||
break
|
||||
|
||||
triggers_sorted = sorted(pnet_triggers, key=lambda t: int(t['lastEvent']['clock']))
|
||||
|
||||
zabbix_logger.info(f"Found {len(triggers_sorted)} triggers for group {group_id}.")
|
||||
|
||||
moskva_tz = timezone('Europe/Moscow')
|
||||
priority_map = {'4': 'HIGH', '5': 'DISASTER'}
|
||||
trigger_messages = []
|
||||
|
||||
for trigger in triggers_sorted:
|
||||
event_time_epoch = int(trigger['lastEvent']['clock'])
|
||||
event_time = datetime.fromtimestamp(event_time_epoch, tz=moskva_tz)
|
||||
description = escape_telegram_chars(trigger['description'])
|
||||
host = trigger['hosts'][0]['name']
|
||||
priority = priority_map.get(trigger['priority'], 'Неизвестно')
|
||||
item_ids = [item['itemid'] for item in trigger['items']]
|
||||
batchgraph_link = f"{ZABBIX_URL}/history.php?action=batchgraph&"
|
||||
batchgraph_link += "&".join([f"itemids[{item_id}]={item_id}" for item_id in item_ids])
|
||||
batchgraph_link += "&graphtype=0"
|
||||
description = description.replace("{HOST.NAME}", host)
|
||||
for i, item in enumerate(trigger['items']):
|
||||
lastvalue_placeholder = f"{{ITEM.LASTVALUE{i + 1}}}"
|
||||
if lastvalue_placeholder in description:
|
||||
description = description.replace(lastvalue_placeholder, item['lastvalue'])
|
||||
event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск')
|
||||
|
||||
severity = severity_map.get(problem['severity'], 'Неизвестно')
|
||||
priority = priority_map.get(problem['severity'], '')
|
||||
description = problem.get('name', 'Нет описания')
|
||||
|
||||
# Получаем хост из описания (или по-другому, если известно)
|
||||
host = extract_host_from_name(description)
|
||||
host_ip = host_ip_map.get(problem.get("hostid"), "Неизвестный IP")
|
||||
|
||||
message = (f"<b>{priority} Host</b>: {host}\n"
|
||||
f"<b>IP</b>: {host_ip}\n"
|
||||
message = (f"<b>Host</b>: {host}\n"
|
||||
f"<b>Описание</b>: {description}\n"
|
||||
f"<b>Критичность</b>: {severity}\n"
|
||||
f"<b>Время создания</b>: {event_time_formatted}")
|
||||
f"<b>Критичность</b>: {priority}\n"
|
||||
f"<b>Время создания</b>: {event_time_formatted}\n"
|
||||
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>')
|
||||
trigger_messages.append(message)
|
||||
|
||||
problem_messages.append(message)
|
||||
|
||||
return problem_messages
|
||||
except Exception as e:
|
||||
telebot.logger.error(f"Error fetching problems for group {group_id}: {e}")
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
zabbix_logger.info(f"Fetched {len(triggers_sorted)} triggers for group {group_id} in {execution_time:.2f} seconds.")
|
||||
return trigger_messages
|
||||
except ZabbixAPIException as e:
|
||||
zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
zabbix_logger.error(f"Error fetching triggers for group {group_id}: {e}")
|
||||
return None
|
||||
139
bot_database.py
@ -1,119 +1,32 @@
|
||||
import hashlib
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
|
||||
import telebot
|
||||
|
||||
from backend_flask import app
|
||||
from config import DB_PATH
|
||||
from models import UserEvents, Users
|
||||
from utilities.database import db
|
||||
|
||||
# Lock for database operations
|
||||
db_lock = Lock()
|
||||
|
||||
|
||||
def init_db():
|
||||
try:
|
||||
# 1️⃣ Проверяем и создаём каталог, если его нет
|
||||
db_dir = os.path.dirname(DB_PATH)
|
||||
if not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True) # Создаём каталог рекурсивно
|
||||
|
||||
# 2️⃣ Проверяем, существует ли файл базы данных
|
||||
db_exists = os.path.exists(DB_PATH)
|
||||
|
||||
# 3️⃣ Открываем соединение, если файла нет, он создастся автоматически
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 4️⃣ Если базы не было, создаём таблицы
|
||||
if not db_exists:
|
||||
cursor.execute('''CREATE TABLE events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hash TEXT UNIQUE,
|
||||
data TEXT,
|
||||
delivered BOOLEAN)''')
|
||||
|
||||
cursor.execute('''CREATE TABLE subscriptions (
|
||||
chat_id INTEGER,
|
||||
region_id TEXT,
|
||||
username TEXT,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
skip BOOLEAN DEFAULT FALSE,
|
||||
disaster_only BOOLEAN DEFAULT FALSE,
|
||||
UNIQUE(chat_id, region_id))''')
|
||||
|
||||
cursor.execute('''CREATE TABLE whitelist (
|
||||
chat_id INTEGER PRIMARY KEY,
|
||||
username TEXT,
|
||||
user_email TEXT)''')
|
||||
|
||||
cursor.execute('''CREATE TABLE admins (
|
||||
chat_id INTEGER PRIMARY KEY,
|
||||
username TEXT)''')
|
||||
|
||||
cursor.execute('''CREATE TABLE regions (
|
||||
region_id TEXT PRIMARY KEY,
|
||||
region_name TEXT,
|
||||
active BOOLEAN DEFAULT TRUE)''')
|
||||
|
||||
cursor.execute('''CREATE TABLE user_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER,
|
||||
username TEXT,
|
||||
action TEXT,
|
||||
timestamp TEXT)''')
|
||||
|
||||
# Добавляем тестовые данные (если их нет)
|
||||
cursor.execute('''INSERT OR IGNORE INTO regions (region_id, region_name) VALUES
|
||||
('01', 'Адыгея'),
|
||||
('02', 'Башкортостан (Уфа)'),
|
||||
('04', 'Алтай'),
|
||||
('19', 'Республика Хакасия')''')
|
||||
|
||||
conn.commit()
|
||||
app.logger.info("✅ Database created and initialized successfully.")
|
||||
else:
|
||||
app.logger.info("✅ Database already exists. Skipping initialization.")
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"❌ Error initializing database: {e}")
|
||||
finally:
|
||||
if 'conn' in locals(): # Проверяем, была ли создана переменная conn
|
||||
conn.close()
|
||||
|
||||
|
||||
def hash_data(data):
|
||||
return hashlib.sha256(str(data).encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def is_whitelisted(chat_id):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?'
|
||||
telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}")
|
||||
cursor.execute(query, (chat_id,))
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
return count > 0
|
||||
|
||||
|
||||
def add_to_whitelist(chat_id, username):
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
query = 'INSERT OR IGNORE INTO whitelist (chat_id, username) VALUES (?, ?)'
|
||||
telebot.logger.info(f"Executing query: {query} with chat_id={chat_id}, username={username}")
|
||||
"""Проверяет, есть ли пользователь с заданным chat_id в базе данных и не заблокирован ли он."""
|
||||
try:
|
||||
cursor.execute(query, (chat_id, username))
|
||||
conn.commit()
|
||||
with app.app_context(): # Создаем контекст приложения
|
||||
user = db.session.query(Users).filter_by(chat_id=chat_id).first()
|
||||
if user:
|
||||
if user.is_blocked:
|
||||
return False, "Ваш доступ заблокирован."
|
||||
return True, None
|
||||
return False, None
|
||||
except Exception as e:
|
||||
telebot.logger.error(f"Error during add to whitelist: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
telebot.logger.error(f"Ошибка при проверке пользователя: {e}")
|
||||
return False, "Произошла ошибка при проверке доступа."
|
||||
|
||||
|
||||
def rundeck_add_to_whitelist(chat_id, username, user_email):
|
||||
@ -221,18 +134,20 @@ def format_regions_list(regions):
|
||||
|
||||
|
||||
def log_user_event(chat_id, username, action):
|
||||
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
"""Логирует действие пользователя с использованием ORM."""
|
||||
try:
|
||||
with db_lock:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)'
|
||||
telebot.logger.debug(
|
||||
f"Executing query: {query} with chat_id={chat_id}, username={username}, action={action}, timestamp={timestamp}")
|
||||
cursor.execute(query, (chat_id, username, action, timestamp))
|
||||
conn.commit()
|
||||
telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {timestamp}.")
|
||||
with app.app_context(): # Создаем контекст приложения
|
||||
timestamp = datetime.now(datetime.UTC) # Оставляем объект datetime для БД
|
||||
formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') # Форматируем для логов
|
||||
|
||||
event = UserEvents(
|
||||
chat_id=chat_id,
|
||||
telegram_id=username,
|
||||
action=action,
|
||||
timestamp=timestamp # В БД передаем объект datetime
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {formatted_time}.")
|
||||
except Exception as e:
|
||||
telebot.logger.error(f"Error logging user event: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
33
config.py
@ -1,11 +1,13 @@
|
||||
# load_dotenv()
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
DEV = os.getenv('DEV')
|
||||
TOKEN = os.getenv('TELEGRAM_TOKEN')
|
||||
ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN')
|
||||
ZABBIX_URL = os.getenv('ZABBIX_URL')
|
||||
DB_PATH = 'db/telezab.db'
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
DB_ABS_PATH = os.path.join(basedir, 'db/telezab.db')
|
||||
SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
|
||||
BASE_URL = '/telezab'
|
||||
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
|
||||
@ -14,9 +16,28 @@ RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
|
||||
RABBITMQ_QUEUE = 'telegram_notifications'
|
||||
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/"
|
||||
|
||||
import os
|
||||
# Настройки LDAP
|
||||
LDAP_HOST = os.getenv('LDAP_HOST', 'localhost')
|
||||
LDAP_PORT = int(os.getenv('LDAP_PORT', 389))
|
||||
LDAP_USE_SSL = os.getenv('LDAP_USE_SSL', 'False').lower() == 'true'
|
||||
LDAP_BASE_DN = os.getenv('LDAP_BASE_DN', 'DC=tech,DC=local')
|
||||
LDAP_BIND_USER_DN = os.getenv('LDAP_BIND_USER_DN', 'CN=sa_tgbot,OU=Service Accounts,DC=tech,DC=local')
|
||||
LDAP_USER_DN = os.getenv('LDAP_USER_DN', 'RMIS')
|
||||
LDAP_USER_PASSWORD = os.getenv('LDAP_USER_PASSWORD', '***')
|
||||
LDAP_USER_OBJECT_FILTER = os.getenv('LDAP_USER_OBJECT_FILTER', '(objectClass=person)')
|
||||
LDAP_USER_RDN_ATTR = os.getenv('LDAP_USER_RDN_ATTR', 'sAMAccountName')
|
||||
LDAP_USER_LOGIN_ATTR = os.getenv('LDAP_USER_LOGIN_ATTR', 'sAMAccountName')
|
||||
LDAP_USER_SEARCH_SCOPE = os.getenv('LDAP_USER_SEARCH_SCOPE', 'SUBTREE')
|
||||
LDAP_SCHEMA = os.getenv('LDAP_SCHEMA', 'active_directory')
|
||||
TZ = os.getenv('TZ', 'Europe/Moscow')
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_ABS_PATH}'
|
||||
|
||||
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', True)
|
||||
SESSION_COOKIE_HTTPONLY = os.getenv('SESSION_COOKIE_HTTPONLY',True)
|
||||
SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE','Lax')
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(seconds=int(os.getenv('PERMANENT_SESSION_LIFETIME_SECONDS', 3600)))
|
||||
SESSION_REFRESH_EACH_REQUEST = os.getenv('SESSION_REFRESH_EACH_REQUEST',False)
|
||||
SESSION_COOKIE_MAX_AGE = os.getenv('SESSION_COOKIE_MAX_AGE',3600)
|
||||
|
||||
class Config:
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_PATH}'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key'
|
||||
@ -1,177 +1,57 @@
|
||||
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 # Импортируем модель региона
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, session
|
||||
from sqlalchemy import create_engine
|
||||
from config import DB_PATH
|
||||
from utilities.database import db
|
||||
from utilities.events_manager import EventManager
|
||||
from utilities.region_manager import RegionManager
|
||||
from utilities.system_manager import SystemManager
|
||||
from utilities.users_manager import UserManager
|
||||
from models import Users
|
||||
from flask_login import logout_user, login_required
|
||||
|
||||
# Создаём 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)
|
||||
|
||||
|
||||
region_manager = RegionManager()
|
||||
user_manager = UserManager(db.session)
|
||||
event_manager = EventManager(db)
|
||||
system_manager = SystemManager()
|
||||
|
||||
|
||||
# Роуты для отображения страниц
|
||||
@bp_dashboard.route('/')
|
||||
# @login_required
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template('index.html')
|
||||
|
||||
@bp_dashboard.route('/users')
|
||||
# @login_required
|
||||
@login_required
|
||||
def users_page():
|
||||
return render_template('users.html')
|
||||
users = Users.query.all()
|
||||
return render_template('users.html', user=users)
|
||||
|
||||
@bp_dashboard.route('/logs')
|
||||
# @login_required
|
||||
@login_required
|
||||
def logs_page():
|
||||
return render_template('logs.html')
|
||||
|
||||
@bp_dashboard.route('/regions')
|
||||
# @login_required
|
||||
@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)
|
||||
@bp_dashboard.route('/health')
|
||||
def healthcheck():
|
||||
pass
|
||||
|
||||
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'})
|
||||
@bp_dashboard.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login'))
|
||||
71
handlers.py
Normal file
@ -0,0 +1,71 @@
|
||||
import telebot
|
||||
from telebot import types
|
||||
|
||||
import backend_bot
|
||||
import bot_database
|
||||
from utilities.telegram_utilities import show_settings_menu
|
||||
|
||||
|
||||
def handle_my_subscriptions_button(message):
|
||||
chat_id = message.chat.id
|
||||
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
|
||||
if not bot_database.is_whitelisted(chat_id):
|
||||
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
|
||||
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
|
||||
return
|
||||
|
||||
user_regions = bot_database.get_user_subscribed_regions(chat_id)
|
||||
if not user_regions:
|
||||
backend_bot.bot.send_message(chat_id, "Вы не подписаны ни на один регион.")
|
||||
telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}")
|
||||
else:
|
||||
user_regions.sort(key=lambda x: int(x[0])) # Сортировка по числовому значению region_id
|
||||
regions_list = bot_database.format_regions_list(user_regions)
|
||||
backend_bot.bot.send_message(chat_id, f"Ваши активные подписки:\n{regions_list}")
|
||||
telebot.logger.debug(f"Запрашиваем {user_regions} for {username} {chat_id}")
|
||||
show_settings_menu(chat_id)
|
||||
|
||||
|
||||
def handle_active_regions_button(message):
|
||||
chat_id = message.chat.id
|
||||
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
|
||||
if not bot_database.is_whitelisted(chat_id):
|
||||
backend_bot.bot.send_message(chat_id, "Вы не авторизованы для использования этого бота.")
|
||||
telebot.logger.info(f"Unauthorized access attempt by {username} {chat_id}")
|
||||
return
|
||||
|
||||
regions = bot_database.get_sorted_regions() # Используем функцию для получения отсортированных регионов
|
||||
if not regions:
|
||||
backend_bot.bot.send_message(chat_id, "Нет активных регионов.")
|
||||
else:
|
||||
regions_list = bot_database.format_regions_list(regions)
|
||||
backend_bot.bot.send_message(chat_id, f"Активные регионы:\n{regions_list}")
|
||||
show_settings_menu(chat_id)
|
||||
|
||||
|
||||
def handle_notification_mode_button(message):
|
||||
chat_id = message.chat.id
|
||||
username = f"@{message.from_user.username}" if message.from_user.username else "N/A"
|
||||
|
||||
telebot.logger.debug(f"Handling notification mode button for user {username} ({chat_id}).")
|
||||
|
||||
if not bot_database.is_whitelisted(chat_id):
|
||||
backend_bot.bot.send_message(chat_id, "Вы неавторизованы для использования этого бота")
|
||||
telebot.logger.warning(f"Unauthorized access attempt by {username} ({chat_id})")
|
||||
return
|
||||
|
||||
# Логируем успешное авторизованное использование бота
|
||||
telebot.logger.info(f"User {username} ({chat_id}) is authorized and is selecting a notification mode.")
|
||||
|
||||
# Отправляем клавиатуру выбора режима уведомлений
|
||||
markup = types.InlineKeyboardMarkup()
|
||||
markup.add(types.InlineKeyboardButton(text="Критические события", callback_data="notification_mode_disaster"))
|
||||
markup.add(types.InlineKeyboardButton(text="Все события", callback_data="notification_mode_all"))
|
||||
|
||||
backend_bot.bot.send_message(chat_id,
|
||||
"Выберите уровень событий мониторинга, уведомление о которых хотите получать:\n"
|
||||
'1. <b>Критические события</b> (приоритет "DISASTER") - события, являющиеся потенциальными авариями и требующие оперативного решения.\nВ Zabbix обязательно имеют тег "CALL" для оперативного привлечения инженеров к устранению.\n\n'
|
||||
'2. <b>Все события (По умолчанию)</b> - критические события, а также события Zabbix высокого ("HIGH") приоритета, имеющие потенциально значительное влияние на сервис и требующее устранение в плановом порядке.',
|
||||
reply_markup=markup, parse_mode="HTML")
|
||||
|
||||
telebot.logger.info(f"Sent notification mode selection message to {username} ({chat_id}).")
|
||||
63
models.py
Normal file
@ -0,0 +1,63 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import PrimaryKeyConstraint, ForeignKey, Integer, String, DateTime
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
|
||||
from utilities.database import db # Импортируем db из backend_flask.py
|
||||
|
||||
class Users(db.Model):
|
||||
chat_id = db.Column(db.Integer, primary_key=True)
|
||||
telegram_id = db.Column(db.String(80), unique=True, nullable=False)
|
||||
user_email = db.Column(db.String(255), unique=True, nullable=False)
|
||||
is_blocked = db.Column(db.Boolean, default=False)
|
||||
subscriptions = relationship("Subscriptions", backref="user", cascade="all, delete-orphan") # Добавлено cascade
|
||||
|
||||
class Regions(db.Model):
|
||||
region_id = db.Column(db.Integer, primary_key=True)
|
||||
region_name = db.Column(db.String(255), nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
|
||||
class Subscriptions(db.Model):
|
||||
region_id = db.Column(db.Integer, nullable=False)
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
skip = db.Column(db.Boolean, default=False)
|
||||
disaster_only = db.Column(db.Boolean, default=False)
|
||||
chat_id = db.Column(db.Integer, ForeignKey('users.chat_id', ondelete='CASCADE'), nullable=False) #Добавляем внешний ключ с ondelete
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint('chat_id', 'region_id'),
|
||||
)
|
||||
|
||||
class UILogs(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chat_id = db.Column(db.Integer, nullable=False)
|
||||
actions = db.Column(db.String(500), nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
|
||||
|
||||
class UserEvents(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chat_id = db.Column(db.Integer, nullable=False)
|
||||
telegram_id = 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())
|
||||
|
||||
class Systems(db.Model):
|
||||
__tablename__ = 'systems'
|
||||
system_id = db.Column(db.String(255), primary_key=True)
|
||||
system_name = db.Column(db.String(255), nullable=False)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<System {self.system_id}: {self.system_name} {self.name}>'
|
||||
|
||||
class WebActionLog(db.Model):
|
||||
__tablename__ = 'web_action_logs'
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
ldap_user_id: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
username: Mapped[str | None] = mapped_column(String(255))
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
action: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
details: Mapped[str | None] = mapped_column(String(1024))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WebActionLog(ldap_user_id='{self.ldap_user_id}', username='{self.username}', action='{self.action}', timestamp='{self.timestamp}')>"
|
||||
@ -3,8 +3,10 @@ aiohappyeyeballs==2.4.6
|
||||
aiohttp==3.11.12
|
||||
aiormq==6.8.1
|
||||
aiosignal==1.3.2
|
||||
alembic==1.15.1
|
||||
attrs==25.1.0
|
||||
blinker==1.9.0
|
||||
cachelib==0.13.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
@ -13,15 +15,19 @@ exceptiongroup==1.2.2
|
||||
Flask==3.1.0
|
||||
flask-ldap3-login==1.0.2
|
||||
Flask-Login==0.6.3
|
||||
Flask-Session==0.8.0
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-WTF==1.2.2
|
||||
frozenlist==1.5.0
|
||||
greenlet==3.1.1
|
||||
gunicorn==23.0.0
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.5
|
||||
ldap3==2.9.1
|
||||
Mako==1.3.9
|
||||
MarkupSafe==3.0.2
|
||||
msgspec==0.19.0
|
||||
multidict==6.1.0
|
||||
packaging==24.2
|
||||
pamqp==3.3.0
|
||||
|
||||
4
static/css/bootstrap-icons-1.11.3/0-circle-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
4
static/css/bootstrap-icons-1.11.3/0-circle.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
4
static/css/bootstrap-icons-1.11.3/0-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
4
static/css/bootstrap-icons-1.11.3/0-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
3
static/css/bootstrap-icons-1.11.3/1-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
3
static/css/bootstrap-icons-1.11.3/1-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
static/css/bootstrap-icons-1.11.3/1-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
4
static/css/bootstrap-icons-1.11.3/1-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
|
||||
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
3
static/css/bootstrap-icons-1.11.3/123.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 854 B |
3
static/css/bootstrap-icons-1.11.3/2-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
static/css/bootstrap-icons-1.11.3/2-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
3
static/css/bootstrap-icons-1.11.3/2-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
4
static/css/bootstrap-icons-1.11.3/2-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
|
||||
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 564 B |
3
static/css/bootstrap-icons-1.11.3/3-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
4
static/css/bootstrap-icons-1.11.3/3-circle.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
3
static/css/bootstrap-icons-1.11.3/3-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 634 B |
4
static/css/bootstrap-icons-1.11.3/3-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
3
static/css/bootstrap-icons-1.11.3/4-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176zM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
4
static/css/bootstrap-icons-1.11.3/4-circle.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
4
static/css/bootstrap-icons-1.11.3/4-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
4
static/css/bootstrap-icons-1.11.3/4-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
3
static/css/bootstrap-icons-1.11.3/5-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
3
static/css/bootstrap-icons-1.11.3/5-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8m15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
3
static/css/bootstrap-icons-1.11.3/5-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
4
static/css/bootstrap-icons-1.11.3/5-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square" viewBox="0 0 16 16">
|
||||
<path d="M7.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 601 B |
3
static/css/bootstrap-icons-1.11.3/6-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c-1.868 0-3.116 1.395-3.116 4.407 0 1.183.228 2.039.597 2.642.569.926 1.477 1.254 2.409 1.254 1.629 0 2.847-1.013 2.847-2.783 0-1.676-1.254-2.555-2.508-2.555-1.125 0-1.752.61-1.98 1.155h-.082c-.012-1.946.727-3.036 1.805-3.036.802 0 1.213.457 1.312.815h1.29c-.06-.908-.962-1.899-2.573-1.899Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
3
static/css/bootstrap-icons-1.11.3/6-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 640 B |
4
static/css/bootstrap-icons-1.11.3/6-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.111 7.863c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm6.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 662 B |
4
static/css/bootstrap-icons-1.11.3/6-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square" viewBox="0 0 16 16">
|
||||
<path d="M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 727 B |
3
static/css/bootstrap-icons-1.11.3/7-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11h3.972v.07L6.025 12H7.42l3.258-6.85V4.002H5.369v1.107Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 251 B |
3
static/css/bootstrap-icons-1.11.3/7-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
static/css/bootstrap-icons-1.11.3/7-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm3.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
4
static/css/bootstrap-icons-1.11.3/7-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square" viewBox="0 0 16 16">
|
||||
<path d="M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
3
static/css/bootstrap-icons-1.11.3/8-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0-1.248-.943-1.84-1.646-1.992v-.065c.598-.187 1.336-.72 1.336-1.781 0-1.225-1.084-2.121-2.654-2.121s-2.66.896-2.66 2.12c0 1.044.709 1.589 1.33 1.782v.065c-.697.152-1.647.732-1.647 2.003 0 1.39 1.19 2.344 2.953 2.344 1.77 0 2.989-.96 2.989-2.355Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 686 B |
3
static/css/bootstrap-icons-1.11.3/8-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.03 1.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 717 B |
4
static/css/bootstrap-icons-1.11.3/8-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.623 6.094c0 .738.586 1.254 1.383 1.254s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23m-.281 3.644c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm8.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 737 B |
4
static/css/bootstrap-icons-1.11.3/8-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-square" viewBox="0 0 16 16">
|
||||
<path d="M10.97 9.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
3
static/css/bootstrap-icons-1.11.3/9-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c2.104 0 3.123-1.464 3.123-4.3 0-3.147-1.459-4.014-2.97-4.014-1.63 0-2.871 1.02-2.871 2.73 0 1.706 1.171 2.667 2.566 2.667 1.06 0 1.7-.557 1.934-1.184h.076c.047 1.67-.475 3.023-1.834 3.023-.71 0-1.149-.363-1.248-.72H5.258c.094.908.926 1.798 2.52 1.798Zm.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 574 B |
3
static/css/bootstrap-icons-1.11.3/9-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.223 4.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3m.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 597 B |
4
static/css/bootstrap-icons-1.11.3/9-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M7.895 8.174c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.777 12.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 620 B |
4
static/css/bootstrap-icons-1.11.3/9-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-9-square" viewBox="0 0 16 16">
|
||||
<path d="M7.777 12.146c-1.593 0-2.425-.89-2.52-1.798h1.296c.1.357.539.72 1.248.72 1.36 0 1.88-1.353 1.834-3.023h-.076c-.235.627-.873 1.184-1.934 1.184-1.395 0-2.566-.961-2.566-2.666 0-1.711 1.242-2.731 2.87-2.731 1.512 0 2.971.867 2.971 4.014 0 2.836-1.02 4.3-3.123 4.3m.118-3.972c.808 0 1.535-.528 1.535-1.594s-.668-1.676-1.56-1.676c-.838 0-1.517.616-1.517 1.659 0 1.072.708 1.61 1.54 1.61Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
3
static/css/bootstrap-icons-1.11.3/activity.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-activity" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-engines-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 0c-.787 0-1.292.592-1.572 1.151A4.35 4.35 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272.313.937a.5.5 0 0 0 .948 0l.405-1.214 2.21-.369.375 2.253-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318.375-2.253 2.21.369.405 1.214a.5.5 0 0 0 .948 0l.313-.937 1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
3
static/css/bootstrap-icons-1.11.3/airplane-engines.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-engines" viewBox="0 0 16 16">
|
||||
<path d="M8 0c-.787 0-1.292.592-1.572 1.151A4.35 4.35 0 0 0 6 3v3.691l-2 1V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.191l-1.17.585A1.5 1.5 0 0 0 0 10.618V12a.5.5 0 0 0 .582.493l1.631-.272.313.937a.5.5 0 0 0 .948 0l.405-1.214 2.21-.369.375 2.253-1.318 1.318A.5.5 0 0 0 5.5 16h5a.5.5 0 0 0 .354-.854l-1.318-1.318.375-2.253 2.21.369.405 1.214a.5.5 0 0 0 .948 0l.313-.937 1.63.272A.5.5 0 0 0 16 12v-1.382a1.5 1.5 0 0 0-.83-1.342L14 8.691V7.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v.191l-2-1V3c0-.568-.14-1.271-.428-1.849C9.292.591 8.787 0 8 0M7 3c0-.432.11-.979.322-1.401C7.542 1.159 7.787 1 8 1s.458.158.678.599C8.889 2.02 9 2.569 9 3v4a.5.5 0 0 0 .276.447l5.448 2.724a.5.5 0 0 1 .276.447v.792l-5.418-.903a.5.5 0 0 0-.575.41l-.5 3a.5.5 0 0 0 .14.437l.646.646H6.707l.647-.646a.5.5 0 0 0 .14-.436l-.5-3a.5.5 0 0 0-.576-.411L1 11.41v-.792a.5.5 0 0 1 .276-.447l5.448-2.724A.5.5 0 0 0 7 7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
static/css/bootstrap-icons-1.11.3/airplane-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918-.375 2.253 1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318-.376-2.253-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 492 B |
3
static/css/bootstrap-icons-1.11.3/airplane.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-airplane" viewBox="0 0 16 16">
|
||||
<path d="M6.428 1.151C6.708.591 7.213 0 8 0s1.292.592 1.572 1.151C9.861 1.73 10 2.431 10 3v3.691l5.17 2.585a1.5 1.5 0 0 1 .83 1.342V12a.5.5 0 0 1-.582.493l-5.507-.918-.375 2.253 1.318 1.318A.5.5 0 0 1 10.5 16h-5a.5.5 0 0 1-.354-.854l1.319-1.318-.376-2.253-5.507.918A.5.5 0 0 1 0 12v-1.382a1.5 1.5 0 0 1 .83-1.342L6 6.691V3c0-.568.14-1.271.428-1.849m.894.448C7.111 2.02 7 2.569 7 3v4a.5.5 0 0 1-.276.447l-5.448 2.724a.5.5 0 0 0-.276.447v.792l5.418-.903a.5.5 0 0 1 .575.41l.5 3a.5.5 0 0 1-.14.437L6.708 15h2.586l-.647-.646a.5.5 0 0 1-.14-.436l.5-3a.5.5 0 0 1 .576-.411L15 11.41v-.792a.5.5 0 0 0-.276-.447L9.276 7.447A.5.5 0 0 1 9 7V3c0-.432-.11-.979-.322-1.401C8.458 1.159 8.213 1 8 1s-.458.158-.678.599"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 840 B |
3
static/css/bootstrap-icons-1.11.3/alarm-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm-fill" viewBox="0 0 16 16">
|
||||
<path d="M6 .5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H9v1.07a7.001 7.001 0 0 1 3.274 12.474l.601.602a.5.5 0 0 1-.707.708l-.746-.746A6.97 6.97 0 0 1 8 16a6.97 6.97 0 0 1-3.422-.892l-.746.746a.5.5 0 0 1-.707-.708l.602-.602A7.001 7.001 0 0 1 7 2.07V1h-.5A.5.5 0 0 1 6 .5m2.5 5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9zM.86 5.387A2.5 2.5 0 1 1 4.387 1.86 8.04 8.04 0 0 0 .86 5.387M11.613 1.86a2.5 2.5 0 1 1 3.527 3.527 8.04 8.04 0 0 0-3.527-3.527"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 615 B |
4
static/css/bootstrap-icons-1.11.3/alarm.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alarm" viewBox="0 0 16 16">
|
||||
<path d="M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9z"/>
|
||||
<path d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1zm1.038 3.018a6 6 0 0 1 .924 0 6 6 0 1 1-.924 0M0 3.5c0 .753.333 1.429.86 1.887A8.04 8.04 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5M13.5 1c-.753 0-1.429.333-1.887.86a8.04 8.04 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 689 B |
3
static/css/bootstrap-icons-1.11.3/alexa.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alexa" viewBox="0 0 16 16">
|
||||
<path d="M7.996 0A8 8 0 0 0 0 8a8 8 0 0 0 6.93 7.93v-1.613a1.06 1.06 0 0 0-.717-1.008A5.6 5.6 0 0 1 2.4 7.865 5.58 5.58 0 0 1 8.054 2.4a5.6 5.6 0 0 1 5.535 5.81l-.002.046-.012.192-.005.061a5 5 0 0 1-.033.284l-.01.068c-.685 4.516-6.564 7.054-6.596 7.068A7.998 7.998 0 0 0 15.992 8 8 8 0 0 0 7.996.001Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
4
static/css/bootstrap-icons-1.11.3/align-bottom.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-bottom" viewBox="0 0 16 16">
|
||||
<rect width="4" height="12" x="6" y="1" rx="1"/>
|
||||
<path d="M1.5 14a.5.5 0 0 0 0 1zm13 1a.5.5 0 0 0 0-1zm-13 0h13v-1h-13z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
3
static/css/bootstrap-icons-1.11.3/align-center.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-center" viewBox="0 0 16 16">
|
||||
<path d="M8 1a.5.5 0 0 1 .5.5V6h-1V1.5A.5.5 0 0 1 8 1m0 14a.5.5 0 0 1-.5-.5V10h1v4.5a.5.5 0 0 1-.5.5M2 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
4
static/css/bootstrap-icons-1.11.3/align-end.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-end" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M14.5 1a.5.5 0 0 0-.5.5v13a.5.5 0 0 0 1 0v-13a.5.5 0 0 0-.5-.5"/>
|
||||
<path d="M13 7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
3
static/css/bootstrap-icons-1.11.3/align-middle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-middle" viewBox="0 0 16 16">
|
||||
<path d="M6 13a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zM1 8a.5.5 0 0 0 .5.5H6v-1H1.5A.5.5 0 0 0 1 8m14 0a.5.5 0 0 1-.5.5H10v-1h4.5a.5.5 0 0 1 .5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
4
static/css/bootstrap-icons-1.11.3/align-start.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-start" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5"/>
|
||||
<path d="M3 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
4
static/css/bootstrap-icons-1.11.3/align-top.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-align-top" viewBox="0 0 16 16">
|
||||
<rect width="4" height="12" rx="1" transform="matrix(1 0 0 -1 6 15)"/>
|
||||
<path d="M1.5 2a.5.5 0 0 1 0-1zm13-1a.5.5 0 0 1 0 1zm-13 0h13v1h-13z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
4
static/css/bootstrap-icons-1.11.3/alipay.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alipay" viewBox="0 0 16 16">
|
||||
<path d="M2.541 0H13.5a2.55 2.55 0 0 1 2.54 2.563v8.297c-.006 0-.531-.046-2.978-.813-.412-.14-.916-.327-1.479-.536q-.456-.17-.957-.353a13 13 0 0 0 1.325-3.373H8.822V4.649h3.831v-.634h-3.83V2.121H7.26c-.274 0-.274.273-.274.273v1.621H3.11v.634h3.875v1.136h-3.2v.634H9.99c-.227.789-.532 1.53-.894 2.202-2.013-.67-4.161-1.212-5.51-.878-.864.214-1.42.597-1.746.998-1.499 1.84-.424 4.633 2.741 4.633 1.872 0 3.675-1.053 5.072-2.787 2.08 1.008 6.37 2.738 6.387 2.745v.105A2.55 2.55 0 0 1 13.5 16H2.541A2.55 2.55 0 0 1 0 13.437V2.563A2.55 2.55 0 0 1 2.541 0"/>
|
||||
<path d="M2.309 9.27c-1.22 1.073-.49 3.034 1.978 3.034 1.434 0 2.868-.925 3.994-2.406-1.602-.789-2.959-1.353-4.425-1.207-.397.04-1.14.217-1.547.58Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 839 B |
3
static/css/bootstrap-icons-1.11.3/alphabet-uppercase.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alphabet-uppercase" viewBox="0 0 16 16">
|
||||
<path d="M1.226 10.88H0l2.056-6.26h1.42l2.047 6.26h-1.29l-.48-1.61H1.707l-.48 1.61ZM2.76 5.818h-.054l-.75 2.532H3.51zm3.217 5.062V4.62h2.56c1.09 0 1.808.582 1.808 1.54 0 .762-.444 1.22-1.05 1.372v.055c.736.074 1.365.587 1.365 1.528 0 1.119-.89 1.766-2.133 1.766zM7.18 5.55v1.675h.8c.812 0 1.171-.308 1.171-.853 0-.51-.328-.822-.898-.822zm0 2.537V9.95h.903c.951 0 1.342-.312 1.342-.909 0-.591-.382-.954-1.095-.954zm5.089-.711v.775c0 1.156.49 1.803 1.347 1.803.705 0 1.163-.454 1.212-1.096H16v.12C15.942 10.173 14.95 11 13.607 11c-1.648 0-2.573-1.073-2.573-2.849v-.78c0-1.775.934-2.871 2.573-2.871 1.347 0 2.34.849 2.393 2.087v.115h-1.172c-.05-.665-.516-1.156-1.212-1.156-.849 0-1.347.67-1.347 1.83"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
3
static/css/bootstrap-icons-1.11.3/alphabet.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alphabet" viewBox="0 0 16 16">
|
||||
<path d="M2.204 11.078c.767 0 1.201-.356 1.406-.737h.059V11h1.216V7.519c0-1.314-.947-1.783-2.11-1.783C1.355 5.736.75 6.42.69 7.27h1.216c.064-.323.313-.552.84-.552s.864.249.864.771v.464H2.346C1.145 7.953.5 8.568.5 9.496c0 .977.693 1.582 1.704 1.582m.42-.947c-.44 0-.845-.235-.845-.718 0-.395.269-.684.84-.684h.991v.538c0 .503-.444.864-.986.864m5.593.937c1.216 0 1.948-.869 1.948-2.31v-.702c0-1.44-.727-2.305-1.929-2.305-.742 0-1.328.347-1.499.889h-.063V3.983h-1.29V11h1.27v-.791h.064c.21.532.776.86 1.499.86Zm-.43-1.025c-.66 0-1.113-.518-1.113-1.28V8.12c0-.825.42-1.343 1.098-1.343.684 0 1.075.518 1.075 1.416v.45c0 .888-.386 1.401-1.06 1.401Zm2.834-1.328c0 1.47.87 2.378 2.305 2.378 1.416 0 2.139-.777 2.158-1.763h-1.186c-.06.425-.313.732-.933.732-.66 0-1.05-.512-1.05-1.352v-.625c0-.81.371-1.328 1.045-1.328.635 0 .879.425.918.776h1.187c-.02-.986-.787-1.806-2.14-1.806-1.41 0-2.304.918-2.304 2.338z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
static/css/bootstrap-icons-1.11.3/alt.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-alt" viewBox="0 0 16 16">
|
||||
<path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5m10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 324 B |
4
static/css/bootstrap-icons-1.11.3/amazon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-amazon" viewBox="0 0 16 16">
|
||||
<path d="M10.813 11.968c.157.083.36.074.5-.05l.005.005a90 90 0 0 1 1.623-1.405c.173-.143.143-.372.006-.563l-.125-.17c-.345-.465-.673-.906-.673-1.791v-3.3l.001-.335c.008-1.265.014-2.421-.933-3.305C10.404.274 9.06 0 8.03 0 6.017 0 3.77.75 3.296 3.24c-.047.264.143.404.316.443l2.054.22c.19-.009.33-.196.366-.387.176-.857.896-1.271 1.703-1.271.435 0 .929.16 1.188.55.264.39.26.91.257 1.376v.432q-.3.033-.621.065c-1.113.114-2.397.246-3.36.67C3.873 5.91 2.94 7.08 2.94 8.798c0 2.2 1.387 3.298 3.168 3.298 1.506 0 2.328-.354 3.489-1.54l.167.246c.274.405.456.675 1.047 1.166ZM6.03 8.431C6.03 6.627 7.647 6.3 9.177 6.3v.57c.001.776.002 1.434-.396 2.133-.336.595-.87.961-1.465.961-.812 0-1.286-.619-1.286-1.533M.435 12.174c2.629 1.603 6.698 4.084 13.183.997.28-.116.475.078.199.431C13.538 13.96 11.312 16 7.57 16 3.832 16 .968 13.446.094 12.386c-.24-.275.036-.4.199-.299z"/>
|
||||
<path d="M13.828 11.943c.567-.07 1.468-.027 1.645.204.135.176-.004.966-.233 1.533-.23.563-.572.961-.762 1.115s-.333.094-.23-.137c.105-.23.684-1.663.455-1.963-.213-.278-1.177-.177-1.625-.13l-.09.009q-.142.013-.233.024c-.193.021-.245.027-.274-.032-.074-.209.779-.556 1.347-.623"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
static/css/bootstrap-icons-1.11.3/amd.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-amd" viewBox="0 0 16 16">
|
||||
<path d="m.334 0 4.358 4.359h7.15v7.15l4.358 4.358V0zM.2 9.72l4.487-4.488v6.281h6.28L6.48 16H.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 230 B |
3
static/css/bootstrap-icons-1.11.3/android.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-android" viewBox="0 0 16 16">
|
||||
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 432 B |
3
static/css/bootstrap-icons-1.11.3/android2.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-android2" viewBox="0 0 16 16">
|
||||
<path d="m10.213 1.471.691-1.26q.069-.124-.048-.192-.128-.057-.195.058l-.7 1.27A4.8 4.8 0 0 0 8.005.941q-1.032 0-1.956.404l-.7-1.27Q5.281-.037 5.154.02q-.117.069-.049.193l.691 1.259a4.25 4.25 0 0 0-1.673 1.476A3.7 3.7 0 0 0 3.5 5.02h9q0-1.125-.623-2.072a4.27 4.27 0 0 0-1.664-1.476ZM6.22 3.303a.37.37 0 0 1-.267.11.35.35 0 0 1-.263-.11.37.37 0 0 1-.107-.264.37.37 0 0 1 .107-.265.35.35 0 0 1 .263-.11q.155 0 .267.11a.36.36 0 0 1 .112.265.36.36 0 0 1-.112.264m4.101 0a.35.35 0 0 1-.262.11.37.37 0 0 1-.268-.11.36.36 0 0 1-.112-.264q0-.154.112-.265a.37.37 0 0 1 .268-.11q.155 0 .262.11a.37.37 0 0 1 .107.265q0 .153-.107.264M3.5 11.77q0 .441.311.75.311.306.76.307h.758l.01 2.182q0 .414.292.703a.96.96 0 0 0 .7.288.97.97 0 0 0 .71-.288.95.95 0 0 0 .292-.703v-2.182h1.343v2.182q0 .414.292.703a.97.97 0 0 0 .71.288.97.97 0 0 0 .71-.288.95.95 0 0 0 .292-.703v-2.182h.76q.436 0 .749-.308.31-.307.311-.75V5.365h-9zm10.495-6.587a.98.98 0 0 0-.702.278.9.9 0 0 0-.293.685v4.063q0 .406.293.69a.97.97 0 0 0 .702.284q.42 0 .712-.284a.92.92 0 0 0 .293-.69V6.146a.9.9 0 0 0-.293-.685 1 1 0 0 0-.712-.278m-12.702.283a1 1 0 0 1 .712-.283q.41 0 .702.283a.9.9 0 0 1 .293.68v4.063a.93.93 0 0 1-.288.69.97.97 0 0 1-.707.284 1 1 0 0 1-.712-.284.92.92 0 0 1-.293-.69V6.146q0-.396.293-.68"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
4
static/css/bootstrap-icons-1.11.3/app-indicator.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app-indicator" viewBox="0 0 16 16">
|
||||
<path d="M5.5 2A3.5 3.5 0 0 0 2 5.5v5A3.5 3.5 0 0 0 5.5 14h5a3.5 3.5 0 0 0 3.5-3.5V8a.5.5 0 0 1 1 0v2.5a4.5 4.5 0 0 1-4.5 4.5h-5A4.5 4.5 0 0 1 1 10.5v-5A4.5 4.5 0 0 1 5.5 1H8a.5.5 0 0 1 0 1z"/>
|
||||
<path d="M16 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
3
static/css/bootstrap-icons-1.11.3/app.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app" viewBox="0 0 16 16">
|
||||
<path d="M11 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
4
static/css/bootstrap-icons-1.11.3/apple.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-apple" viewBox="0 0 16 16">
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282"/>
|
||||
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
static/css/bootstrap-icons-1.11.3/archive-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive-fill" viewBox="0 0 16 16">
|
||||
<path d="M12.643 15C13.979 15 15 13.845 15 12.5V5H1v7.5C1 13.845 2.021 15 3.357 15zM5.5 7h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1 0-1M.8 1a.8.8 0 0 0-.8.8V3a.8.8 0 0 0 .8.8h14.4A.8.8 0 0 0 16 3V1.8a.8.8 0 0 0-.8-.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
3
static/css/bootstrap-icons-1.11.3/archive.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5zm13-3H1v2h14zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 394 B |
3
static/css/bootstrap-icons-1.11.3/arrow-90deg-down.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-down" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4.854 14.854a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V3.5A2.5 2.5 0 0 1 6.5 1h8a.5.5 0 0 1 0 1h-8A1.5 1.5 0 0 0 5 3.5v9.793l3.146-3.147a.5.5 0 0 1 .708.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 345 B |
3
static/css/bootstrap-icons-1.11.3/arrow-90deg-left.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.146 4.854a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H12.5A2.5 2.5 0 0 1 15 6.5v8a.5.5 0 0 1-1 0v-8A1.5 1.5 0 0 0 12.5 5H2.707l3.147 3.146a.5.5 0 1 1-.708.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
3
static/css/bootstrap-icons-1.11.3/arrow-90deg-right.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M14.854 4.854a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 4H3.5A2.5 2.5 0 0 0 1 6.5v8a.5.5 0 0 0 1 0v-8A1.5 1.5 0 0 1 3.5 5h9.793l-3.147 3.146a.5.5 0 0 0 .708.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
3
static/css/bootstrap-icons-1.11.3/arrow-90deg-up.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-90deg-up" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4.854 1.146a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L4 2.707V12.5A2.5 2.5 0 0 0 6.5 15h8a.5.5 0 0 0 0-1h-8A1.5 1.5 0 0 1 5 12.5V2.707l3.146 3.147a.5.5 0 1 0 .708-.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 344 B |
3
static/css/bootstrap-icons-1.11.3/arrow-bar-down.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-down" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 3.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5M8 6a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 0 1 .708-.708L7.5 12.293V6.5A.5.5 0 0 1 8 6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
3
static/css/bootstrap-icons-1.11.3/arrow-bar-left.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5M10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
3
static/css/bootstrap-icons-1.11.3/arrow-bar-right.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 8a.5.5 0 0 0 .5.5h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L12.293 7.5H6.5A.5.5 0 0 0 6 8m-2.5 7a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
3
static/css/bootstrap-icons-1.11.3/arrow-bar-up.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-bar-up" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 10a.5.5 0 0 0 .5-.5V3.707l2.146 2.147a.5.5 0 0 0 .708-.708l-3-3a.5.5 0 0 0-.708 0l-3 3a.5.5 0 1 0 .708.708L7.5 3.707V9.5a.5.5 0 0 0 .5.5m-7 2.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
4
static/css/bootstrap-icons-1.11.3/arrow-clockwise.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 316 B |
3
static/css/bootstrap-icons-1.11.3/arrow-down-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-circle" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M8.5 4.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-5.904-2.803a.5.5 0 1 1 .707.707L6.707 10h2.768a.5.5 0 0 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.525a.5.5 0 0 1 1 0v2.768z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-circle" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-5.904-2.854a.5.5 0 1 1 .707.708L6.707 9.95h2.768a.5.5 0 1 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.475a.5.5 0 1 1 1 0v2.768z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 16a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2zm8.096-10.803L6 9.293V6.525a.5.5 0 0 0-1 0V10.5a.5.5 0 0 0 .5.5h3.975a.5.5 0 0 0 0-1H6.707l4.096-4.096a.5.5 0 1 0-.707-.707"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left-square" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm10.096 3.146a.5.5 0 1 1 .707.708L6.707 9.95h2.768a.5.5 0 1 1 0 1H5.5a.5.5 0 0 1-.5-.5V6.475a.5.5 0 1 1 1 0v2.768z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 435 B |
3
static/css/bootstrap-icons-1.11.3/arrow-down-left.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2 13.5a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 0-1H3.707L13.854 2.854a.5.5 0 0 0-.708-.708L3 12.293V7.5a.5.5 0 0 0-1 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m5.904-2.803a.5.5 0 1 0-.707.707L9.293 10H6.525a.5.5 0 0 0 0 1H10.5a.5.5 0 0 0 .5-.5V6.525a.5.5 0 0 0-1 0v2.768z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-circle" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M5.854 5.146a.5.5 0 1 0-.708.708L9.243 9.95H6.475a.5.5 0 1 0 0 1h3.975a.5.5 0 0 0 .5-.5V6.475a.5.5 0 1 0-1 0v2.768z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-right-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M14 16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2zM5.904 5.197 10 9.293V6.525a.5.5 0 0 1 1 0V10.5a.5.5 0 0 1-.5.5H6.525a.5.5 0 0 1 0-1h2.768L5.197 5.904a.5.5 0 0 1 .707-.707"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 361 B |