Говорила мне мама...ну нахер эти ваши HDD

This commit is contained in:
Udo Chudo 2025-04-21 12:04:20 +05:00
parent 9e2560f7c3
commit b94e8d4724
2090 changed files with 16136 additions and 999 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.12.3-slim FROM python:3.13.1-slim
LABEL authors="UdoChudo" LABEL authors="UdoChudo"
# Установим необходимые пакеты # Установим необходимые пакеты
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
@ -9,23 +9,29 @@ RUN apt-get update && apt-get install -y \
sqlite3 \ sqlite3 \
curl \ curl \
telnet \ telnet \
supervisor \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Установим рабочую директорию # Установим рабочую директорию
WORKDIR /app WORKDIR /app
# Скопируем файлы проекта # Скопируем файлы проекта
COPY . /app COPY . /app
# Установим зависимости проекта # Копируем конфигурацию supervisord
RUN pip install --no-cache-dir -r requirements.txt 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 EXPOSE 5000
ENV TZ=Europe/Moscow ENV TZ=Europe/Moscow
ENV FLASK_APP telezab.py ENV FLASK_APP telezab.py
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
# Запуск Gunicorn
CMD ["python3", "telezab.py"]
# Указываем команду для запуска supervisord
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

262
backend/api.py Normal file
View 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
View 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')

View File

@ -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, \ from bot_database import get_admins, is_whitelisted, format_regions_list, get_sorted_regions, log_user_event, \
get_user_subscribed_regions get_user_subscribed_regions
from config import DB_PATH 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 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): def handle_main_menu(message, chat_id, text):

View File

@ -1,54 +1,69 @@
import logging import logging
import sqlite3 import sqlite3
import telebot from flask import Flask, request, jsonify, redirect, url_for
from flask import Flask, request, jsonify, render_template, flash, redirect, url_for from flask_login import LoginManager
from flask_ldap3_login.forms import LDAPLoginForm
from flask_login import login_manager, login_user, logout_user, UserMixin
from frontend.dashboard import bp_dashboard, bp_api import config
from frontend.dashboard import bp_dashboard
import backend_bot from backend.api import bp_api
import bot_database from backend.auth import bp_auth, User
import telezab
import utilities.telegram_utilities as telegram_util
from backend_locks import db_lock from backend_locks import db_lock
from config import BASE_URL, DB_PATH from config import DB_PATH, TZ
from utilities.telegram_utilities import extract_region_number, format_message, validate_chat_id, validate_telegram_id, validate_email 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') login_manager = LoginManager()
# 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"
# Пользовательский класс def create_app():
class User(UserMixin): app = Flask(__name__, static_url_path='/telezab/static', template_folder='templates')
def __init__(self, dn, username): app.config['SECRET_KEY'] = config.SECRET_KEY # Замените на надежный секретный ключ
self.id = dn app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE # Убедитесь, что установлено значение True
self.username = username 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('/telezab/webhook', methods=['POST'])
@app.route(BASE_URL + '/webhook', methods=['POST'])
def webhook(): def webhook():
try: try:
# Получаем данные и логируем # Получаем данные и логируем
data = request.get_json() data = request.get_json()
app.logger.info(f"Получены данные: {data}") app.logger.info(f"Получены данные: {data}")
# # Генерация хеша события и логирование
# event_hash = bot_database.hash_data(data)
# app.logger.debug(f"Сгенерирован хеш для события: {event_hash}")
# Работа с базой данных в блоке синхронизации # Работа с базой данных в блоке синхронизации
with db_lock: with db_lock:
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
@ -135,228 +150,3 @@ def webhook():
app.logger.error(f"Неожиданная ошибка: {e}") app.logger.error(f"Неожиданная ошибка: {e}")
return jsonify({"status": "error", "message": "Внутренняя ошибка сервера"}), 500 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

View File

@ -1,23 +1,35 @@
import logging
import re import re
import time import time
from datetime import datetime from datetime import datetime
import telebot
from pytz import timezone from pytz import timezone
from pyzabbix import ZabbixAPI from pyzabbix import ZabbixAPI, ZabbixAPIException
import backend_bot import backend_bot
from config import ZABBIX_URL, ZABBIX_API_TOKEN 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): def get_triggers_for_group(chat_id, group_id):
triggers = get_zabbix_triggers(group_id) # Получаем все активные события без периода try:
if not triggers: triggers = get_zabbix_triggers(group_id)
backend_bot.bot.send_message(chat_id, f"Нет активных событий.") if not triggers:
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) show_main_menu(chat_id)
else:
send_triggers_to_user(triggers, chat_id)
def get_triggers_for_all_groups(chat_id, region_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 = [] all_triggers = []
for group in filtered_groups: for group in filtered_groups:
triggers = get_zabbix_triggers(group['groupid']) try:
if triggers: triggers = get_zabbix_triggers(group['groupid'])
all_triggers.extend(triggers) 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: if all_triggers:
send_triggers_to_user(all_triggers, chat_id) 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: 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) 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: 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) show_main_menu(chat_id)
@ -56,73 +82,86 @@ def extract_host_from_name(name):
def get_zabbix_triggers(group_id): def get_zabbix_triggers(group_id):
pnet_mediatypes = {"Pnet integration JS 2025", "Pnet integration JS 2024", "Pnet integration new2"}
start_time = time.time()
try: try:
zapi = ZabbixAPI(ZABBIX_URL) zapi = ZabbixAPI(ZABBIX_URL)
zapi.login(api_token=ZABBIX_API_TOKEN) 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( problems = zapi.problem.get(
output=["eventid", "name", "severity", "clock"], severities=[4, 5],
hostids=host_ids,
suppressed=0, suppressed=0,
acknowledged=0, acknowledged=0,
filter={"severity": ["4", "5"]}, # Только высокий и аварийный уровень groupids=group_id
sortorder="ASC" )
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: events = zapi.event.get(
telebot.logger.info(f"No active problems found for group {group_id}") severities=[4, 5],
return [] objectids=trigger_ids,
select_alerts="mediatype"
# Получение IP-адресов хостов
host_interfaces = zapi.hostinterface.get(
hostids=host_ids,
output=["hostid", "ip"]
) )
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: pnet_triggers = []
event_time_epoch = int(problem['clock']) event_dict = {event["objectid"]: event for event in events}
event_time = datetime.fromtimestamp(event_time_epoch, tz=moscow_tz)
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 Мск') event_time_formatted = event_time.strftime('%Y-%m-%d %H:%M:%S Мск')
message = (f"<b>Host</b>: {host}\n"
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"
f"<b>Описание</b>: {description}\n" f"<b>Описание</b>: {description}\n"
f"<b>Критичность</b>: {severity}\n" f"<b>Критичность</b>: {priority}\n"
f"<b>Время создания</b>: {event_time_formatted}") f"<b>Время создания</b>: {event_time_formatted}\n"
f'<b>URL</b>: <a href="{batchgraph_link}">Ссылка на график</a>')
trigger_messages.append(message)
problem_messages.append(message) end_time = time.time()
execution_time = end_time - start_time
return problem_messages zabbix_logger.info(f"Fetched {len(triggers_sorted)} triggers for group {group_id} in {execution_time:.2f} seconds.")
except Exception as e: return trigger_messages
telebot.logger.error(f"Error fetching problems for group {group_id}: {e}") except ZabbixAPIException as e:
zabbix_logger.error(f"Zabbix API error for group {group_id}: {e}")
return None return None
except Exception as e:
zabbix_logger.error(f"Error fetching triggers for group {group_id}: {e}")
return None

View File

@ -1,119 +1,32 @@
import hashlib
import os
import sqlite3 import sqlite3
import time from datetime import datetime
from threading import Lock from threading import Lock
import telebot import telebot
from backend_flask import app from backend_flask import app
from config import DB_PATH from config import DB_PATH
from models import UserEvents, Users
from utilities.database import db
# Lock for database operations # Lock for database operations
db_lock = Lock() 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): def is_whitelisted(chat_id):
with db_lock: """Проверяет, есть ли пользователь с заданным chat_id в базе данных и не заблокирован ли он."""
conn = sqlite3.connect(DB_PATH) try:
cursor = conn.cursor() with app.app_context(): # Создаем контекст приложения
query = 'SELECT COUNT(*) FROM whitelist WHERE chat_id = ?' user = db.session.query(Users).filter_by(chat_id=chat_id).first()
telebot.logger.debug(f"Executing query: {query} with chat_id={chat_id}") if user:
cursor.execute(query, (chat_id,)) if user.is_blocked:
count = cursor.fetchone()[0] return False, "Ваш доступ заблокирован."
conn.close() return True, None
return count > 0 return False, None
except Exception as e:
telebot.logger.error(f"Ошибка при проверке пользователя: {e}")
def add_to_whitelist(chat_id, username): return False, "Произошла ошибка при проверке доступа."
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}")
try:
cursor.execute(query, (chat_id, username))
conn.commit()
except Exception as e:
telebot.logger.error(f"Error during add to whitelist: {e}")
finally:
conn.close()
def rundeck_add_to_whitelist(chat_id, username, user_email): 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): def log_user_event(chat_id, username, action):
timestamp = time.strftime('%Y-%m-%d %H:%M:%S') """Логирует действие пользователя с использованием ORM."""
try: try:
with db_lock: with app.app_context(): # Создаем контекст приложения
conn = sqlite3.connect(DB_PATH) timestamp = datetime.now(datetime.UTC) # Оставляем объект datetime для БД
cursor = conn.cursor() formatted_time = timestamp.strftime('%Y-%m-%d %H:%M:%S') # Форматируем для логов
query = 'INSERT INTO user_events (chat_id, username, action, timestamp) VALUES (?, ?, ?, ?)'
telebot.logger.debug( event = UserEvents(
f"Executing query: {query} with chat_id={chat_id}, username={username}, action={action}, timestamp={timestamp}") chat_id=chat_id,
cursor.execute(query, (chat_id, username, action, timestamp)) telegram_id=username,
conn.commit() action=action,
telebot.logger.info(f"User event logged: {chat_id} ({username}) - {action} at {timestamp}.") 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: except Exception as e:
telebot.logger.error(f"Error logging user event: {e}") telebot.logger.error(f"Error logging user event: {e}")
finally:
conn.close()

View File

@ -1,11 +1,13 @@
# load_dotenv()
import os import os
from datetime import timedelta
DEV = os.getenv('DEV') DEV = os.getenv('DEV')
TOKEN = os.getenv('TELEGRAM_TOKEN') TOKEN = os.getenv('TELEGRAM_TOKEN')
ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN') ZABBIX_API_TOKEN = os.getenv('ZABBIX_API_TOKEN')
ZABBIX_URL = os.getenv('ZABBIX_URL') ZABBIX_URL = os.getenv('ZABBIX_URL')
DB_PATH = 'db/telezab.db' 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" SUPPORT_EMAIL = "shiftsupport-rtmis@rtmis.ru"
BASE_URL = '/telezab' BASE_URL = '/telezab'
RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') RABBITMQ_HOST = os.getenv('RABBITMQ_HOST')
@ -14,9 +16,28 @@ RABBITMQ_PASS = os.getenv('RABBITMQ_PASS')
RABBITMQ_QUEUE = 'telegram_notifications' RABBITMQ_QUEUE = 'telegram_notifications'
RABBITMQ_URL_FULL = f"amqp://{RABBITMQ_LOGIN}:{RABBITMQ_PASS}@{RABBITMQ_HOST}/" 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'

View File

@ -1,177 +1,57 @@
from flask import Blueprint, render_template, jsonify, request, redirect, url_for import logging
from flask_login import login_required
from sqlalchemy.orm import sessionmaker from flask import Blueprint, render_template, redirect, url_for, session
from sqlalchemy import create_engine, text from sqlalchemy import create_engine
from config import DB_PATH, BASE_URL from config import DB_PATH
from .models import Region # Импортируем модель региона 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 # Создаём Blueprint
bp_dashboard = Blueprint('dashboard', __name__, url_prefix='/telezab/') 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}') 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('/') @bp_dashboard.route('/')
# @login_required @login_required
def dashboard(): def dashboard():
return render_template('index.html') return render_template('index.html')
@bp_dashboard.route('/users') @bp_dashboard.route('/users')
# @login_required @login_required
def users_page(): def users_page():
return render_template('users.html') users = Users.query.all()
return render_template('users.html', user=users)
@bp_dashboard.route('/logs') @bp_dashboard.route('/logs')
# @login_required @login_required
def logs_page(): def logs_page():
return render_template('logs.html') return render_template('logs.html')
@bp_dashboard.route('/regions') @bp_dashboard.route('/regions')
# @login_required @login_required
def regions_page(): def regions_page():
return render_template('regions.html') return render_template('regions.html')
# Роуты для API @bp_dashboard.route('/health')
@bp_api.route('/users', methods=['GET']) def healthcheck():
def get_users(): pass
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
session = Session() @bp_dashboard.route('/logout')
query = text(""" @login_required
SELECT w.chat_id, w.username, w.user_email ,s.region_id, s.disaster_only def logout():
FROM whitelist w logout_user()
LEFT JOIN subscriptions s ON w.chat_id = s.chat_id AND s.active = 1 session.clear()
""") return redirect(url_for('auth.login'))
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'})

71
handlers.py Normal file
View 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
View 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}')>"

View File

@ -3,8 +3,10 @@ aiohappyeyeballs==2.4.6
aiohttp==3.11.12 aiohttp==3.11.12
aiormq==6.8.1 aiormq==6.8.1
aiosignal==1.3.2 aiosignal==1.3.2
alembic==1.15.1
attrs==25.1.0 attrs==25.1.0
blinker==1.9.0 blinker==1.9.0
cachelib==0.13.0
certifi==2025.1.31 certifi==2025.1.31
charset-normalizer==3.4.1 charset-normalizer==3.4.1
click==8.1.8 click==8.1.8
@ -13,15 +15,19 @@ exceptiongroup==1.2.2
Flask==3.1.0 Flask==3.1.0
flask-ldap3-login==1.0.2 flask-ldap3-login==1.0.2
Flask-Login==0.6.3 Flask-Login==0.6.3
Flask-Session==0.8.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==3.1.1
Flask-WTF==1.2.2 Flask-WTF==1.2.2
frozenlist==1.5.0 frozenlist==1.5.0
greenlet==3.1.1 greenlet==3.1.1
gunicorn==23.0.0
idna==3.10 idna==3.10
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.5 Jinja2==3.1.5
ldap3==2.9.1 ldap3==2.9.1
Mako==1.3.9
MarkupSafe==3.0.2 MarkupSafe==3.0.2
msgspec==0.19.0
multidict==6.1.0 multidict==6.1.0
packaging==24.2 packaging==24.2
pamqp==3.3.0 pamqp==3.3.0

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

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

View 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

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

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

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

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

View 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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More