feat(admin): add admin panel and reminder system

This commit is contained in:
2026-07-30 19:46:16 +05:00
parent ecc006287b
commit e79624a13a
17 changed files with 582 additions and 148 deletions
+129 -29
View File
@@ -8,10 +8,19 @@ from datetime import datetime
from typing import List, Optional, Union
from aiogram import Bot
from aiogram.types import (InlineKeyboardButton, KeyboardButton, WebAppInfo, Message, InlineKeyboardMarkup,
ReplyKeyboardMarkup)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
WebAppInfo,
Message,
KeyboardButton,
ReplyKeyboardMarkup,
CallbackQuery,
)
from pytz import timezone
from tgbot.database.db_settings import SettingsRepository
from tgbot.data.config import get_admins, BOT_TIMEZONE
from tgbot.utils.misc.bot_logging import bot_logger
@@ -24,11 +33,11 @@ def rkb(text: str) -> KeyboardButton:
# Быстрая сборка инлайн-кнопки
def ikb(
text: str,
data: Optional[str] = None,
url: Optional[str] = None,
switch: Optional[str] = None,
web: Optional[str] = None,
text: str,
data: Optional[str] = None,
url: Optional[str] = None,
switch: Optional[str] = None,
web: Optional[str] = None,
) -> InlineKeyboardButton:
if data is not None:
return InlineKeyboardButton(text=text, callback_data=data)
@@ -52,11 +61,11 @@ async def del_message(message: Message):
# Отправка текста с фото, если оно передано или обычным сообщением
async def smart_message(
bot: Bot,
user_id: int,
text: str,
keyboard: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
photo: Optional[str] = None,
bot: Bot,
user_id: int,
text: str,
keyboard: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
photo: Optional[str] = None,
):
if photo is not None and photo.title() != "None":
await bot.send_photo(
@@ -85,7 +94,9 @@ async def send_admins(bot: Bot, text: str, markup=None, not_me=0):
disable_web_page_preview=True,
)
except Exception:
bot_logger.warning("Не удалось отправить сообщение админу %s", admin, exc_info=True)
bot_logger.warning(
"Не удалось отправить сообщение админу %s", admin, exc_info=True
)
################################## РАЗНОЕ ######################################
@@ -108,7 +119,7 @@ def clear_list(get_list: list) -> list:
# Делит список на части нужного размера
def split_list(get_list: list, count: int) -> List[list]:
return [get_list[i:i + count] for i in range(0, len(get_list), count)]
return [get_list[i : i + count] for i in range(0, len(get_list), count)]
# Возвращает текущую дату, при full=True еще и время
@@ -137,11 +148,17 @@ def convert_date(from_time, full=True, second=True) -> Union[str, int]:
if from_time.isdigit():
from_timestamp = int(from_time)
if full:
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y %H:%M:%S")
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime(
"%d.%m.%Y %H:%M:%S"
)
elif second:
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y %H:%M")
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime(
"%d.%m.%Y %H:%M"
)
else:
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y")
to_time = datetime.fromtimestamp(from_timestamp, bot_timezone).strftime(
"%d.%m.%Y"
)
else:
parts = from_time.split()
@@ -215,20 +232,21 @@ def gen_password(len_password: int = 16, type_password: str = "default") -> str:
# Склоняет единицы времени под число
def convert_times(get_time: int, get_type: str = "day") -> str:
get_time = int(get_time)
if get_time < 0: get_time = 0
if get_time < 0:
get_time = 0
if get_type == "second":
get_list = ['секунда', 'секунды', 'секунд']
get_list = ["секунда", "секунды", "секунд"]
elif get_type == "minute":
get_list = ['минута', 'минуты', 'минут']
get_list = ["минута", "минуты", "минут"]
elif get_type == "hour":
get_list = ['час', 'часа', 'часов']
get_list = ["час", "часа", "часов"]
elif get_type == "day":
get_list = ['день', 'дня', 'дней']
get_list = ["день", "дня", "дней"]
elif get_type == "month":
get_list = ['месяц', 'месяца', 'месяцев']
get_list = ["месяц", "месяца", "месяцев"]
else:
get_list = ['год', 'года', 'лет']
get_list = ["год", "года", "лет"]
if get_time % 10 == 1 and get_time % 100 != 11:
count = 0
@@ -244,9 +262,9 @@ def convert_times(get_time: int, get_type: str = "day") -> str:
def is_bool(value: Union[bool, str, int]) -> bool:
value = str(value).strip().lower()
if value in ('y', 'yes', 't', 'true', 'on', '1'):
if value in ("y", "yes", "t", "true", "on", "1"):
return True
elif value in ('n', 'no', 'f', 'false', 'off', '0'):
elif value in ("n", "no", "f", "false", "off", "0"):
return False
else:
raise ValueError(f"Некорректное bool-значение: {value}")
@@ -266,9 +284,11 @@ def snum(amount: Union[int, float], remains: int = 2) -> str:
str_amount = str_amount[:remains_save]
if "." in str(str_amount):
while str(str_amount).endswith('0'): str_amount = str(str_amount)[:-1]
while str(str_amount).endswith("0"):
str_amount = str(str_amount)[:-1]
if str(str_amount).endswith('.'): str_amount = str(str_amount)[:-1]
if str(str_amount).endswith("."):
str_amount = str(str_amount)[:-1]
return str(str_amount)
@@ -299,7 +319,8 @@ def is_number(get_number: Union[str, int, float]) -> bool:
if str(get_number).isdigit():
return True
else:
if "," in str(get_number): get_number = str(get_number).replace(",", ".")
if "," in str(get_number):
get_number = str(get_number).replace(",", ".")
try:
float(get_number)
@@ -318,3 +339,82 @@ def format_rate(amount: Union[float, int], around: int = 2) -> str:
response = response.rstrip("0").rstrip(".")
return response
# ─── Вспомогательные функции ─────────────────────────────────────────────────
def _interval_label(hours: int) -> str:
labels = {0: "выкл", 5: "5 ч", 12: "12 ч", 24: "24 ч"}
return labels.get(hours, f"{hours} ч")
def _status_icon(flag: bool) -> str:
return "" if flag else ""
def _panel_text(s) -> str:
remind = _interval_label(s.remind_interval)
status = _status_icon(s.status_work)
msg_info = s.misc_message or "не задано"
btn_info = (
f"{s.misc_finl_text}{s.misc_finl_link}"
if s.misc_finl_text and s.misc_finl_link
else "нет"
)
return (
"⚙️ <b>Панель управления</b>\n\n"
f"<b>🤖 Статус бота</b>: {status}\n"
f"<b>🔔 Напоминания</b>: {remind}\n"
f"<b>📝 Сообщение</b>: <blockquote>{msg_info}\n</blockquote>\n"
f"<b>🔗 Кнопка</b>: <i>{btn_info}</i>"
)
def _panel_kb(s) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
status_label = "🔴 Выключить бота" if s.status_work else "🟢 Включить бота"
kb.row(ikb(status_label, data="adm:toggle"))
kb.row(ikb("✏️ Изменить сообщение", data="adm:set_msg"))
kb.row(ikb("🔔 Интервал напоминаний", data="adm:interval"))
if s.misc_finl_text and s.misc_finl_link:
kb.row(
ikb("🔗 Изменить кнопку", data="adm:set_btn"),
ikb("🗑 Убрать кнопку", data="adm:clr_btn"),
)
else:
kb.row(ikb("🔗 Добавить кнопку", data="adm:set_btn"))
kb.row(ikb("✖️ Закрыть", data="close_this"))
return kb.as_markup()
def _interval_kb(current: int) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
for h in (5, 12, 24):
mark = "" if current == h else ""
kb.add(ikb(f"{mark}{_interval_label(h)}", data=f"adm:int:{h}"))
mark = "" if current == 0 else ""
kb.add(ikb(f"{mark}Выкл", data="adm:int:0"))
kb.adjust(3, 1)
kb.row(ikb("◀ Назад", data="adm:main"))
return kb.as_markup()
async def _show_panel(target, settings=None) -> None:
"""Отправить или обновить панель (target — Message или CallbackQuery)."""
if settings is None:
settings = await SettingsRepository().get()
text = _panel_text(settings)
kb = _panel_kb(settings)
if isinstance(target, CallbackQuery):
try:
await target.message.edit_text(text, reply_markup=kb)
except Exception:
await target.message.answer(text, reply_markup=kb)
await target.answer()
else:
await target.answer(text, reply_markup=kb)