generated from FOSS/Bot-Template
feat(admin): add admin panel and reminder system
This commit is contained in:
@@ -7,7 +7,13 @@ from aiogram import Bot, Dispatcher
|
|||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
|
|
||||||
from tgbot.data.config import BOT_DATABASE_EXPORT, BOT_TOKEN, BOT_SCHEDULER, get_admins, validate_bot_config
|
from tgbot.data.config import (
|
||||||
|
BOT_DATABASE_EXPORT,
|
||||||
|
BOT_TOKEN,
|
||||||
|
BOT_SCHEDULER,
|
||||||
|
get_admins,
|
||||||
|
validate_bot_config,
|
||||||
|
)
|
||||||
from tgbot.database.core import close_database
|
from tgbot.database.core import close_database
|
||||||
from tgbot.database.repository import prepare_database
|
from tgbot.database.repository import prepare_database
|
||||||
from tgbot.middlewares import register_all_middlewares
|
from tgbot.middlewares import register_all_middlewares
|
||||||
@@ -17,7 +23,7 @@ from tgbot.utils.misc.bot_commands import set_commands
|
|||||||
from tgbot.utils.misc.bot_logging import bot_logger
|
from tgbot.utils.misc.bot_logging import bot_logger
|
||||||
from tgbot.utils.misc_functions import autobackup_admin, startup_notify
|
from tgbot.utils.misc_functions import autobackup_admin, startup_notify
|
||||||
|
|
||||||
# Включаем мгновенный вывод print() без flush=True в каждом вызове
|
|
||||||
def configure_console_output() -> None:
|
def configure_console_output() -> None:
|
||||||
for stream in (sys.stdout, sys.stderr):
|
for stream in (sys.stdout, sys.stderr):
|
||||||
if hasattr(stream, "reconfigure"):
|
if hasattr(stream, "reconfigure"):
|
||||||
@@ -29,7 +35,7 @@ colorama.init()
|
|||||||
|
|
||||||
|
|
||||||
# Запуск задач по расписанию
|
# Запуск задач по расписанию
|
||||||
async def scheduler_start(bot):
|
async def scheduler_start(bot: Bot) -> None:
|
||||||
if BOT_DATABASE_EXPORT:
|
if BOT_DATABASE_EXPORT:
|
||||||
BOT_SCHEDULER.add_job(
|
BOT_SCHEDULER.add_job(
|
||||||
autobackup_admin,
|
autobackup_admin,
|
||||||
@@ -42,6 +48,20 @@ async def scheduler_start(bot):
|
|||||||
misfire_grace_time=60,
|
misfire_grace_time=60,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Проверяем очередь напоминаний каждые 30 минут
|
||||||
|
from tgbot.routers.channel_requests import send_reminders
|
||||||
|
|
||||||
|
BOT_SCHEDULER.add_job(
|
||||||
|
send_reminders,
|
||||||
|
trigger="interval",
|
||||||
|
minutes=30,
|
||||||
|
args=(bot,),
|
||||||
|
id="send_reminders",
|
||||||
|
replace_existing=True,
|
||||||
|
coalesce=True,
|
||||||
|
misfire_grace_time=120,
|
||||||
|
)
|
||||||
|
|
||||||
if not BOT_SCHEDULER.running:
|
if not BOT_SCHEDULER.running:
|
||||||
BOT_SCHEDULER.start()
|
BOT_SCHEDULER.start()
|
||||||
|
|
||||||
@@ -49,39 +69,41 @@ async def scheduler_start(bot):
|
|||||||
# Запуск бота и базовой обвязки
|
# Запуск бота и базовой обвязки
|
||||||
async def main():
|
async def main():
|
||||||
validate_bot_config()
|
validate_bot_config()
|
||||||
await prepare_database() # Проверка готовности БД
|
await prepare_database()
|
||||||
|
|
||||||
dp = Dispatcher() # Диспетчер событий
|
dp = Dispatcher()
|
||||||
arSession = AsyncRequestSession() # Общая сессия aiohttp
|
arSession = AsyncRequestSession()
|
||||||
|
|
||||||
bot = Bot( # Образ Бота
|
bot = Bot(
|
||||||
token=BOT_TOKEN,
|
token=BOT_TOKEN,
|
||||||
default=DefaultBotProperties(
|
default=DefaultBotProperties(
|
||||||
parse_mode=ParseMode.HTML
|
parse_mode=ParseMode.HTML, link_preview_is_disabled=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
register_all_middlewares(dp) # Подключение мидлварей
|
register_all_middlewares(dp)
|
||||||
register_all_routers(dp) # Подключение роутера
|
register_all_routers(dp)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await set_commands(bot) # Обновление команды в Telegram
|
await set_commands(bot)
|
||||||
await startup_notify(bot) # Сообщаем админам о старте
|
await startup_notify(bot)
|
||||||
await scheduler_start(bot) # Подключение задач по расписанию
|
await scheduler_start(bot)
|
||||||
|
|
||||||
bot_info = await bot.get_me()
|
bot_info = await bot.get_me()
|
||||||
bot_logger.info("Бот запущен: @%s", bot_info.username)
|
bot_logger.info("Бот запущен: @%s", bot_info.username)
|
||||||
print(colorama.Fore.LIGHTYELLOW_EX + f"~~~~~ Бот запущен - @{bot_info.username} ~~~~~")
|
print(
|
||||||
|
colorama.Fore.LIGHTYELLOW_EX
|
||||||
|
+ f"~~~~~ Бот запущен - @{bot_info.username} ~~~~~"
|
||||||
|
)
|
||||||
print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ TG developer - @djimbox ~~~~~")
|
print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ TG developer - @djimbox ~~~~~")
|
||||||
print(colorama.Fore.RESET)
|
print(colorama.Fore.RESET)
|
||||||
|
|
||||||
if len(get_admins()) == 0:
|
if len(get_admins()) == 0:
|
||||||
print("***** УКАЖИТЕ BOT_ADMIN_IDS В .env *****")
|
print("***** УКАЖИТЕ BOT_ADMIN_IDS В .env *****")
|
||||||
|
|
||||||
await bot.delete_webhook() # Сбрасывание вебхука, если он был
|
await bot.delete_webhook()
|
||||||
await bot.get_updates(offset=-1) # Чистка старых апдейтов
|
await bot.get_updates(offset=-1)
|
||||||
|
|
||||||
# Запуск бота (polling режим)
|
|
||||||
await dp.start_polling(
|
await dp.start_polling(
|
||||||
bot,
|
bot,
|
||||||
arSession=arSession,
|
arSession=arSession,
|
||||||
@@ -91,9 +113,9 @@ async def main():
|
|||||||
if BOT_SCHEDULER.running:
|
if BOT_SCHEDULER.running:
|
||||||
BOT_SCHEDULER.shutdown(wait=False)
|
BOT_SCHEDULER.shutdown(wait=False)
|
||||||
|
|
||||||
await arSession.close() # Закрытие сессии aiohttp
|
await arSession.close()
|
||||||
await bot.session.close() # Закрытие сессии API Telegram
|
await bot.session.close()
|
||||||
await close_database() # Закрытие соединений с БД
|
await close_database()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# - *- coding: utf- 8 - *-
|
# - *- coding: utf- 8 - *-
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from pydantic import AliasChoices, Field, field_validator
|
from pydantic import AliasChoices, Field, field_validator
|
||||||
@@ -24,7 +24,7 @@ class Settings(BaseSettings):
|
|||||||
bot_token: str = Field(
|
bot_token: str = Field(
|
||||||
default="", validation_alias=AliasChoices("BOT_TOKEN", "bot_token")
|
default="", validation_alias=AliasChoices("BOT_TOKEN", "bot_token")
|
||||||
)
|
)
|
||||||
admin_ids: List[int] = Field(
|
admin_ids: Any = Field(
|
||||||
default_factory=list, validation_alias=AliasChoices("BOT_ADMIN_IDS", "admin_id")
|
default_factory=list, validation_alias=AliasChoices("BOT_ADMIN_IDS", "admin_id")
|
||||||
)
|
)
|
||||||
database_export: bool = Field(
|
database_export: bool = Field(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from .db_pending import PendingModel, PendingRepository
|
||||||
from .db_settings import SettingsModel, SettingsRepository
|
from .db_settings import SettingsModel, SettingsRepository
|
||||||
from .db_users import UserModel, UsersRepository
|
from .db_users import UserModel, UsersRepository
|
||||||
|
|
||||||
ModelSettings = SettingsModel
|
ModelSettings = SettingsModel
|
||||||
ModelUsers = UserModel
|
ModelUsers = UserModel
|
||||||
|
ModelPending = PendingModel
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# - *- coding: utf- 8 - *-
|
||||||
|
from sqlalchemy import BigInteger, Integer
|
||||||
|
from sqlalchemy import update as sqlalchemy_update
|
||||||
|
from sqlalchemy.dialects.sqlite import insert
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from tgbot.database.core import Base, session_scope
|
||||||
|
from tgbot.database.repository import BaseRepository
|
||||||
|
from tgbot.utils.const_functions import get_unix
|
||||||
|
|
||||||
|
|
||||||
|
# Модель для хранения пользователей, которым бот отправил приветствие
|
||||||
|
# и которые ещё не вышли из очереди напоминаний
|
||||||
|
class PendingModel(Base):
|
||||||
|
__tablename__ = "storage_pending"
|
||||||
|
|
||||||
|
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
|
||||||
|
joined_at: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||||
|
last_remind_at: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
|
||||||
|
ModelBase = PendingModel
|
||||||
|
BaseModel = PendingModel
|
||||||
|
|
||||||
|
|
||||||
|
# Репозиторий для управления очередью напоминаний
|
||||||
|
class PendingRepository(BaseRepository[PendingModel]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.table_model = PendingModel
|
||||||
|
self.storage_name = PendingModel.__tablename__
|
||||||
|
|
||||||
|
# Добавляем пользователя в очередь, если его ещё нет
|
||||||
|
async def add(self, user_id: int) -> None:
|
||||||
|
statement = insert(PendingModel).values(
|
||||||
|
user_id=user_id,
|
||||||
|
joined_at=get_unix(),
|
||||||
|
last_remind_at=0,
|
||||||
|
)
|
||||||
|
statement = statement.on_conflict_do_nothing(index_elements=[PendingModel.user_id])
|
||||||
|
|
||||||
|
async with session_scope() as session:
|
||||||
|
await session.execute(statement)
|
||||||
|
|
||||||
|
# Обновляем время последнего напоминания
|
||||||
|
async def update_remind_at(self, user_id: int) -> None:
|
||||||
|
async with session_scope() as session:
|
||||||
|
await session.execute(
|
||||||
|
sqlalchemy_update(PendingModel)
|
||||||
|
.where(PendingModel.user_id == user_id)
|
||||||
|
.values(last_remind_at=get_unix())
|
||||||
|
)
|
||||||
@@ -18,6 +18,10 @@ class SettingsModel(Base):
|
|||||||
status_notify: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
status_notify: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
misc_message: Mapped[str] = mapped_column(String(4096), nullable=True, default=None)
|
misc_message: Mapped[str] = mapped_column(String(4096), nullable=True, default=None)
|
||||||
|
misc_message_media: Mapped[str] = mapped_column(
|
||||||
|
String(255), nullable=True, default=None
|
||||||
|
)
|
||||||
|
|
||||||
misc_finl_text: Mapped[str] = mapped_column(
|
misc_finl_text: Mapped[str] = mapped_column(
|
||||||
String(255), nullable=True, default=None
|
String(255), nullable=True, default=None
|
||||||
)
|
)
|
||||||
@@ -25,6 +29,9 @@ class SettingsModel(Base):
|
|||||||
String(255), nullable=True, default=None
|
String(255), nullable=True, default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Интервал напоминаний в часах: 0 = выключены, допустимые значения: 5, 12, 24
|
||||||
|
remind_interval: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
|
|
||||||
ModelBase = SettingsModel
|
ModelBase = SettingsModel
|
||||||
BaseModel = SettingsModel
|
BaseModel = SettingsModel
|
||||||
|
|||||||
@@ -87,20 +87,46 @@ class BaseRepository(Generic[ModelTranslator]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Готовим подключение к БД. Сами миграции запускаются отдельно через migrate.py.
|
# Готовим подключение к БД: создаём таблицы и накатываем недостающие колонки.
|
||||||
async def prepare_database() -> None:
|
async def prepare_database() -> None:
|
||||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Импортируем модели, чтобы репозитории работали с уже загруженными таблицами
|
# Импортируем модели, чтобы Base.metadata знала обо всех таблицах
|
||||||
import tgbot.database # noqa: F401
|
import tgbot.database # noqa: F401
|
||||||
|
|
||||||
|
from tgbot.database.core import Base, engine
|
||||||
from tgbot.database.db_settings import SettingsRepository
|
from tgbot.database.db_settings import SettingsRepository
|
||||||
|
|
||||||
|
# Создаём все таблицы (если их ещё нет)
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
# Мягкая миграция: добавляем колонку remind_interval если её нет
|
||||||
|
await _migrate_settings_columns()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await SettingsRepository().ensure_default()
|
await SettingsRepository().ensure_default()
|
||||||
except SQLAlchemyError as error:
|
except SQLAlchemyError as error:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"База данных не готова. Запусти миграции командой: python migrate.py"
|
"База данных не готова — проверь подключение к SQLite"
|
||||||
) from error
|
) from error
|
||||||
|
|
||||||
bot_logger.info("База данных готова")
|
bot_logger.info("База данных готова")
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_settings_columns() -> None:
|
||||||
|
"""Добавляет новые колонки в storage_settings для существующих БД."""
|
||||||
|
from sqlalchemy import text
|
||||||
|
from tgbot.database.core import engine
|
||||||
|
|
||||||
|
migrations = [
|
||||||
|
"ALTER TABLE storage_settings ADD COLUMN remind_interval INTEGER NOT NULL DEFAULT 0",
|
||||||
|
]
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
for sql in migrations:
|
||||||
|
try:
|
||||||
|
await conn.execute(text(sql))
|
||||||
|
except Exception:
|
||||||
|
# Колонка уже существует — игнорируем
|
||||||
|
pass
|
||||||
|
|||||||
@@ -5,31 +5,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|||||||
from tgbot.utils.const_functions import ikb
|
from tgbot.utils.const_functions import ikb
|
||||||
|
|
||||||
|
|
||||||
# Инлайн-клавиатура для пользователя
|
def _build_msg_keyboard(finl_text, finl_link):
|
||||||
def user_finl() -> InlineKeyboardMarkup:
|
if finl_text and finl_link:
|
||||||
keyboard = InlineKeyboardBuilder()
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.row(ikb(finl_text, url=finl_link))
|
||||||
keyboard.row(
|
return kb.as_markup()
|
||||||
ikb("Действие", data="user_inline_x"),
|
return None
|
||||||
ikb("Раздел", data="user_inline:user_btn"),
|
|
||||||
ikb("Скоро", data="..."),
|
|
||||||
).row(
|
|
||||||
ikb("Неизвестная кнопка", data="unknown"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return keyboard.as_markup()
|
|
||||||
|
|
||||||
|
|
||||||
# Инлайн-клавиатура для админа
|
|
||||||
def admin_finl() -> InlineKeyboardMarkup:
|
|
||||||
keyboard = InlineKeyboardBuilder()
|
|
||||||
|
|
||||||
keyboard.row(
|
|
||||||
ikb("Действие", data="admin_inline_x"),
|
|
||||||
ikb("Раздел", data="admin_inline:admin_btn"),
|
|
||||||
ikb("Скоро", data="..."),
|
|
||||||
).row(
|
|
||||||
ikb("Неизвестная кнопка", data="unknown"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return keyboard.as_markup()
|
|
||||||
|
|||||||
+16
-11
@@ -2,8 +2,9 @@
|
|||||||
from aiogram import Dispatcher
|
from aiogram import Dispatcher
|
||||||
|
|
||||||
from tgbot.routers import main_errors, main_missed, main_start
|
from tgbot.routers import main_errors, main_missed, main_start
|
||||||
|
from tgbot.routers import channel_requests
|
||||||
from tgbot.routers.admin import admin_menu
|
from tgbot.routers.admin import admin_menu
|
||||||
from tgbot.routers.user import user_menu
|
from tgbot.routers.admin import admin_panel
|
||||||
from tgbot.utils.misc.bot_filters import IsAdmin, IsPrivate
|
from tgbot.utils.misc.bot_filters import IsAdmin, IsPrivate
|
||||||
|
|
||||||
|
|
||||||
@@ -13,8 +14,9 @@ def register_all_routers(dp: Dispatcher):
|
|||||||
main_errors.router.message.filter(IsPrivate())
|
main_errors.router.message.filter(IsPrivate())
|
||||||
main_start.router.message.filter(IsPrivate())
|
main_start.router.message.filter(IsPrivate())
|
||||||
|
|
||||||
user_menu.router.message.filter(IsPrivate())
|
# Панель /admin — только для приватных чатов и только для админов
|
||||||
user_menu.router.callback_query.filter(IsPrivate())
|
admin_panel.router.message.filter(IsPrivate(), IsAdmin())
|
||||||
|
admin_panel.router.callback_query.filter(IsPrivate(), IsAdmin())
|
||||||
|
|
||||||
admin_menu.router.message.filter(IsPrivate(), IsAdmin())
|
admin_menu.router.message.filter(IsPrivate(), IsAdmin())
|
||||||
admin_menu.router.callback_query.filter(IsPrivate(), IsAdmin())
|
admin_menu.router.callback_query.filter(IsPrivate(), IsAdmin())
|
||||||
@@ -22,13 +24,16 @@ def register_all_routers(dp: Dispatcher):
|
|||||||
main_missed.router.message.filter(IsPrivate())
|
main_missed.router.message.filter(IsPrivate())
|
||||||
main_missed.router.callback_query.filter(IsPrivate())
|
main_missed.router.callback_query.filter(IsPrivate())
|
||||||
|
|
||||||
# Базовые роутеры, которые нужны всегда
|
# channel_requests не фильтруем по IsPrivate — заявки приходят от каналов
|
||||||
dp.include_router(main_errors.router) # Ошибки
|
dp.include_router(channel_requests.router) # ChatJoinRequest
|
||||||
dp.include_router(main_start.router) # Старт и главное меню
|
|
||||||
|
|
||||||
# Роутеры для пользователей и админов
|
# Базовые роутеры
|
||||||
dp.include_router(user_menu.router) # Пользовательские обработчики
|
dp.include_router(main_errors.router)
|
||||||
dp.include_router(admin_menu.router) # Админские обработчики
|
dp.include_router(main_start.router)
|
||||||
|
|
||||||
# Обработка всего, что не поймали выше
|
# Пользователи и админы
|
||||||
dp.include_router(main_missed.router) # Пропущенные сообщения и колбэки
|
dp.include_router(admin_panel.router) # /admin панель (раньше admin_menu)
|
||||||
|
dp.include_router(admin_menu.router) # утилиты: /db, /log, /clear_log
|
||||||
|
|
||||||
|
# Всё, что не поймали выше
|
||||||
|
dp.include_router(main_missed.router)
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ from pathlib import Path
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
from aiogram import Router, Bot, F
|
from aiogram import Router, Bot, F
|
||||||
from aiogram.filters import Command
|
from aiogram.filters import Command
|
||||||
from aiogram.types import FSInputFile, Message, CallbackQuery
|
from aiogram.types import FSInputFile, Message
|
||||||
from aiogram.utils.media_group import MediaGroupBuilder
|
from aiogram.utils.media_group import MediaGroupBuilder
|
||||||
|
|
||||||
from tgbot.data.config import BOT_DATABASE_EXPORT, PATH_DATABASE, PATH_LOGS
|
from tgbot.data.config import BOT_DATABASE_EXPORT, PATH_DATABASE, PATH_LOGS
|
||||||
from tgbot.database.db_users import UserModel
|
from tgbot.database.db_users import UserModel
|
||||||
from tgbot.keyboards.inline_main import admin_finl
|
|
||||||
from tgbot.utils.const_functions import get_date
|
from tgbot.utils.const_functions import get_date
|
||||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||||
|
|
||||||
|
from tgbot.utils.const_functions import _show_panel
|
||||||
|
|
||||||
router = Router(name=__name__)
|
router = Router(name=__name__)
|
||||||
LOGS_DIR = Path(PATH_LOGS).parent
|
LOGS_DIR = Path(PATH_LOGS).parent
|
||||||
SERVICE_LOG_FILES = (
|
SERVICE_LOG_FILES = (
|
||||||
@@ -21,14 +22,12 @@ SERVICE_LOG_FILES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Кнопка админского меню
|
# ─── Команда /admin ──────────────────────────────────────────────────────────
|
||||||
|
@router.message(Command("admin"))
|
||||||
@router.message(F.text == "Админ-меню")
|
@router.message(F.text == "Админ-меню")
|
||||||
async def admin_button_inline(
|
async def cmd_admin(message: Message, state: FSM, **_) -> None:
|
||||||
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
|
||||||
):
|
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
await _show_panel(message)
|
||||||
await message.answer("Инлайн-клавиатура админа", reply_markup=admin_finl())
|
|
||||||
|
|
||||||
|
|
||||||
# Отправка файла базы админам
|
# Отправка файла базы админам
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# - *- coding: utf- 8 - *-
|
||||||
|
import html as _html
|
||||||
|
|
||||||
|
from aiogram import F, Router
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
from tgbot.database.db_settings import SettingsRepository
|
||||||
|
from tgbot.utils.const_functions import ikb, _interval_label, _interval_kb, _show_panel
|
||||||
|
from tgbot.utils.misc.bot_models import FSM
|
||||||
|
|
||||||
|
router = Router(name=__name__)
|
||||||
|
|
||||||
|
ALLOWED_INTERVALS = (0, 5, 12, 24)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── FSM ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class AdminStates(StatesGroup):
|
||||||
|
wait_message = State() # ждём новое приветственное сообщение
|
||||||
|
wait_btn_text = State() # ждём текст inline-кнопки
|
||||||
|
wait_btn_link = State() # ждём ссылку inline-кнопки
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Команда /admin ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("admin"))
|
||||||
|
async def cmd_admin(message: Message, state: FSM, **_) -> None:
|
||||||
|
await state.clear()
|
||||||
|
await _show_panel(message)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Главное меню (возврат из подменю) ───────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:main")
|
||||||
|
async def adm_main(call: CallbackQuery, state: FSM, **_) -> None:
|
||||||
|
await state.clear()
|
||||||
|
await _show_panel(call)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Включить / выключить бота ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:toggle")
|
||||||
|
async def adm_toggle(call: CallbackQuery, **_) -> None:
|
||||||
|
repo = SettingsRepository()
|
||||||
|
settings = await repo.get()
|
||||||
|
await repo.update(status_work=not settings.status_work)
|
||||||
|
await _show_panel(call)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Изменение приветственного сообщения ─────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:set_msg")
|
||||||
|
async def adm_set_msg(call: CallbackQuery, state: FSM, **_) -> None:
|
||||||
|
await state.set_state(AdminStates.wait_message)
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.row(ikb("◀ Отмена", data="adm:cancel"))
|
||||||
|
await call.message.edit_text(
|
||||||
|
"📝 <b>Отправьте новое приветственное сообщение</b>\n\n"
|
||||||
|
"• Просто текст <b>или</b> фото с подписью (caption)\n"
|
||||||
|
"• HTML-разметка поддерживается\n\n"
|
||||||
|
"Чтобы отменить — нажмите кнопку ниже.",
|
||||||
|
reply_markup=kb.as_markup(),
|
||||||
|
)
|
||||||
|
await call.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminStates.wait_message)
|
||||||
|
async def adm_recv_message(message: Message, state: FSM, **_) -> None:
|
||||||
|
repo = SettingsRepository()
|
||||||
|
if message.photo:
|
||||||
|
await repo.update(
|
||||||
|
misc_message=message.caption or "",
|
||||||
|
misc_message_media=message.photo[-1].file_id,
|
||||||
|
)
|
||||||
|
elif message.text:
|
||||||
|
await repo.update(misc_message=message.text, misc_message_media=None)
|
||||||
|
else:
|
||||||
|
await message.answer("❌ Поддерживаются только текст и фото с подписью")
|
||||||
|
return
|
||||||
|
await state.clear()
|
||||||
|
settings = await repo.get()
|
||||||
|
await message.answer("✅ Сообщение сохранено")
|
||||||
|
await _show_panel(message, settings)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Интервал напоминаний ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:interval")
|
||||||
|
async def adm_interval_menu(call: CallbackQuery, **_) -> None:
|
||||||
|
settings = await SettingsRepository().get()
|
||||||
|
await call.message.edit_text(
|
||||||
|
"🔔 <b>Интервал напоминаний</b>\n\n"
|
||||||
|
"Бот повторно отправит сообщение через выбранный промежуток.\n"
|
||||||
|
"<b>Выкл</b> — напоминания не отправляются.",
|
||||||
|
reply_markup=_interval_kb(settings.remind_interval),
|
||||||
|
)
|
||||||
|
await call.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("adm:int:"))
|
||||||
|
async def adm_set_interval(call: CallbackQuery, **_) -> None:
|
||||||
|
try:
|
||||||
|
hours = int(call.data.split(":")[2])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
await call.answer("Неверное значение", show_alert=True)
|
||||||
|
return
|
||||||
|
if hours not in ALLOWED_INTERVALS:
|
||||||
|
await call.answer("Неверное значение", show_alert=True)
|
||||||
|
return
|
||||||
|
await SettingsRepository().update(remind_interval=hours)
|
||||||
|
await call.answer(f"✅ Интервал: {_interval_label(hours)}")
|
||||||
|
await _show_panel(call)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Кнопка в приветственном сообщении ───────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:set_btn")
|
||||||
|
async def adm_set_btn(call: CallbackQuery, state: FSM, **_) -> None:
|
||||||
|
await state.set_state(AdminStates.wait_btn_text)
|
||||||
|
kb = InlineKeyboardBuilder()
|
||||||
|
kb.row(ikb("◀ Отмена", data="adm:cancel"))
|
||||||
|
await call.message.edit_text(
|
||||||
|
"🔗 <b>Введите текст кнопки</b>\n\nНапример: <code>Перейти на сайт</code>",
|
||||||
|
reply_markup=kb.as_markup(),
|
||||||
|
)
|
||||||
|
await call.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminStates.wait_btn_text, F.text)
|
||||||
|
async def adm_recv_btn_text(message: Message, state: FSM, **_) -> None:
|
||||||
|
await state.update_data(btn_text=message.text.strip())
|
||||||
|
await state.set_state(AdminStates.wait_btn_link)
|
||||||
|
await message.answer(
|
||||||
|
"🔗 <b>Теперь введите ссылку для кнопки</b>\n\n"
|
||||||
|
"Например: <code>https://example.com</code>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(AdminStates.wait_btn_link, F.text)
|
||||||
|
async def adm_recv_btn_link(message: Message, state: FSM, **_) -> None:
|
||||||
|
data = await state.get_data()
|
||||||
|
btn_text = data.get("btn_text", "")
|
||||||
|
btn_link = message.text.strip()
|
||||||
|
if not btn_link.startswith(("http://", "https://", "tg://")):
|
||||||
|
await message.answer(
|
||||||
|
"❌ Ссылка должна начинаться с <code>http://</code>, "
|
||||||
|
"<code>https://</code> или <code>tg://</code>"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
repo = SettingsRepository()
|
||||||
|
await repo.update(misc_finl_text=btn_text, misc_finl_link=btn_link)
|
||||||
|
await state.clear()
|
||||||
|
settings = await repo.get()
|
||||||
|
await message.answer("✅ Кнопка сохранена")
|
||||||
|
await _show_panel(message, settings)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:clr_btn")
|
||||||
|
async def adm_clr_btn(call: CallbackQuery, **_) -> None:
|
||||||
|
await SettingsRepository().update(misc_finl_text=None, misc_finl_link=None)
|
||||||
|
await call.answer("🗑 Кнопка удалена")
|
||||||
|
await _show_panel(call)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Отмена текущего действия ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "adm:cancel")
|
||||||
|
async def adm_cancel(call: CallbackQuery, state: FSM, **_) -> None:
|
||||||
|
await state.clear()
|
||||||
|
await _show_panel(call)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# - *- coding: utf- 8 - *-
|
||||||
|
from aiogram import Bot, Router
|
||||||
|
from aiogram.exceptions import TelegramForbiddenError
|
||||||
|
from aiogram.types import ChatJoinRequest
|
||||||
|
|
||||||
|
from tgbot.database.db_pending import PendingRepository
|
||||||
|
from tgbot.database.db_settings import SettingsRepository
|
||||||
|
from tgbot.utils.const_functions import get_unix, smart_message
|
||||||
|
from tgbot.utils.misc.bot_logging import bot_logger
|
||||||
|
|
||||||
|
from tgbot.keyboards.inline_main import _build_msg_keyboard
|
||||||
|
|
||||||
|
router = Router(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.chat_join_request()
|
||||||
|
async def on_join_request(update: ChatJoinRequest, bot: Bot) -> None:
|
||||||
|
user_id = update.from_user.id
|
||||||
|
settings = await SettingsRepository().get()
|
||||||
|
|
||||||
|
if not settings.status_work:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not settings.misc_message:
|
||||||
|
return
|
||||||
|
|
||||||
|
keyboard = _build_msg_keyboard(settings.misc_finl_text, settings.misc_finl_link)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await smart_message(
|
||||||
|
bot=bot,
|
||||||
|
user_id=user_id,
|
||||||
|
text=settings.misc_message,
|
||||||
|
keyboard=keyboard,
|
||||||
|
photo=settings.misc_message_media,
|
||||||
|
)
|
||||||
|
|
||||||
|
except TelegramForbiddenError:
|
||||||
|
bot_logger.info("Пользователь %s заблокировал бота, пропускаем", user_id)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
bot_logger.warning(
|
||||||
|
"Не удалось отправить сообщение пользователю %s", user_id, exc_info=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if settings.remind_interval > 0:
|
||||||
|
await PendingRepository().add(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_reminders(bot: Bot) -> None:
|
||||||
|
settings = await SettingsRepository().get()
|
||||||
|
|
||||||
|
if settings.remind_interval == 0 or not settings.misc_message:
|
||||||
|
return
|
||||||
|
|
||||||
|
interval_seconds = settings.remind_interval * 3600
|
||||||
|
now = get_unix()
|
||||||
|
pending_repo = PendingRepository()
|
||||||
|
pending_users = await pending_repo.get_all()
|
||||||
|
|
||||||
|
keyboard = _build_msg_keyboard(settings.misc_finl_text, settings.misc_finl_link)
|
||||||
|
|
||||||
|
for row in pending_users:
|
||||||
|
reference = row.last_remind_at if row.last_remind_at > 0 else row.joined_at
|
||||||
|
|
||||||
|
if now - reference < interval_seconds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
await smart_message(
|
||||||
|
bot=bot,
|
||||||
|
user_id=row.user_id,
|
||||||
|
text=settings.misc_message,
|
||||||
|
keyboard=keyboard,
|
||||||
|
photo=settings.misc_message_media,
|
||||||
|
)
|
||||||
|
await pending_repo.update_remind_at(user_id=row.user_id)
|
||||||
|
|
||||||
|
except TelegramForbiddenError:
|
||||||
|
await pending_repo.delete(user_id=row.user_id)
|
||||||
|
bot_logger.info(
|
||||||
|
"Пользователь %s заблокировал бота, удалён из очереди", row.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
bot_logger.warning(
|
||||||
|
"Не удалось отправить напоминание пользователю %s",
|
||||||
|
row.user_id,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
@@ -26,4 +26,4 @@ class MessageNotModifiedHandler(ErrorHandler):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
raise self.event
|
raise self.exception
|
||||||
|
|||||||
+15
-10
@@ -4,25 +4,30 @@ from aiogram.filters import Command
|
|||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
from tgbot.database.db_users import UserModel
|
from tgbot.database.db_users import UserModel
|
||||||
from tgbot.keyboards.reply_main import menu_frep
|
from tgbot.database.db_settings import SettingsRepository
|
||||||
from tgbot.utils.const_functions import ded
|
from tgbot.utils.const_functions import smart_message
|
||||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||||
|
|
||||||
|
from tgbot.keyboards.inline_main import _build_msg_keyboard
|
||||||
|
|
||||||
router = Router(name=__name__)
|
router = Router(name=__name__)
|
||||||
|
|
||||||
|
|
||||||
# Главное меню пользователя
|
# Главное меню пользователя
|
||||||
# @router.message(F.text.in_(("menu", "return", "start")))
|
@router.message(F.text.in_({"menu", "return", "start"}))
|
||||||
# @router.message(Command(commands=["start"]))
|
@router.message(Command(commands=["start", "menu"]))
|
||||||
async def main_start(
|
async def main_start(
|
||||||
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
):
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(
|
settings = await SettingsRepository().get()
|
||||||
ded(f"""
|
keyboard = _build_msg_keyboard(settings.misc_finl_text, settings.misc_finl_link)
|
||||||
🔸 Привет, {User.user_name}
|
|
||||||
🔸 Введи /start или /menu
|
await smart_message(
|
||||||
"""),
|
bot=bot,
|
||||||
reply_markup=menu_frep(message.from_user.id),
|
user_id=message.from_user.id,
|
||||||
|
text=settings.misc_message,
|
||||||
|
keyboard=keyboard,
|
||||||
|
photo=settings.misc_message_media,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# - *- coding: utf- 8 - *-
|
|
||||||
from aiogram import Router, Bot, F
|
|
||||||
from aiogram.filters import Command
|
|
||||||
from aiogram.types import Message, CallbackQuery
|
|
||||||
|
|
||||||
from tgbot.database.db_users import UserModel
|
|
||||||
from tgbot.keyboards.inline_main import user_finl
|
|
||||||
from tgbot.keyboards.reply_main import menu_frep
|
|
||||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
|
||||||
|
|
||||||
router = Router(name=__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# Кнопка пользовательского меню
|
|
||||||
# @router.message(F.text == "Пользовательское меню")
|
|
||||||
async def user_button_inline(
|
|
||||||
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
|
||||||
):
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
await message.answer("Инлайн-клавиатура пользователя", reply_markup=user_finl())
|
|
||||||
|
|
||||||
|
|
||||||
# Команда возврата в меню
|
|
||||||
# @router.message(Command(commands="menu"))
|
|
||||||
async def user_command_menu(
|
|
||||||
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
|
||||||
):
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
await message.answer(
|
|
||||||
"Команда /menu открывает главное меню",
|
|
||||||
reply_markup=menu_frep(message.from_user.id),
|
|
||||||
)
|
|
||||||
+129
-29
@@ -8,10 +8,19 @@ from datetime import datetime
|
|||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.types import (InlineKeyboardButton, KeyboardButton, WebAppInfo, Message, InlineKeyboardMarkup,
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
ReplyKeyboardMarkup)
|
from aiogram.types import (
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
WebAppInfo,
|
||||||
|
Message,
|
||||||
|
KeyboardButton,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
CallbackQuery,
|
||||||
|
)
|
||||||
from pytz import timezone
|
from pytz import timezone
|
||||||
|
|
||||||
|
from tgbot.database.db_settings import SettingsRepository
|
||||||
from tgbot.data.config import get_admins, BOT_TIMEZONE
|
from tgbot.data.config import get_admins, BOT_TIMEZONE
|
||||||
from tgbot.utils.misc.bot_logging import bot_logger
|
from tgbot.utils.misc.bot_logging import bot_logger
|
||||||
|
|
||||||
@@ -24,11 +33,11 @@ def rkb(text: str) -> KeyboardButton:
|
|||||||
|
|
||||||
# Быстрая сборка инлайн-кнопки
|
# Быстрая сборка инлайн-кнопки
|
||||||
def ikb(
|
def ikb(
|
||||||
text: str,
|
text: str,
|
||||||
data: Optional[str] = None,
|
data: Optional[str] = None,
|
||||||
url: Optional[str] = None,
|
url: Optional[str] = None,
|
||||||
switch: Optional[str] = None,
|
switch: Optional[str] = None,
|
||||||
web: Optional[str] = None,
|
web: Optional[str] = None,
|
||||||
) -> InlineKeyboardButton:
|
) -> InlineKeyboardButton:
|
||||||
if data is not None:
|
if data is not None:
|
||||||
return InlineKeyboardButton(text=text, callback_data=data)
|
return InlineKeyboardButton(text=text, callback_data=data)
|
||||||
@@ -52,11 +61,11 @@ async def del_message(message: Message):
|
|||||||
|
|
||||||
# Отправка текста с фото, если оно передано или обычным сообщением
|
# Отправка текста с фото, если оно передано или обычным сообщением
|
||||||
async def smart_message(
|
async def smart_message(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
text: str,
|
text: str,
|
||||||
keyboard: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
|
keyboard: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
|
||||||
photo: Optional[str] = None,
|
photo: Optional[str] = None,
|
||||||
):
|
):
|
||||||
if photo is not None and photo.title() != "None":
|
if photo is not None and photo.title() != "None":
|
||||||
await bot.send_photo(
|
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,
|
disable_web_page_preview=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
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]:
|
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 еще и время
|
# Возвращает текущую дату, при full=True еще и время
|
||||||
@@ -137,11 +148,17 @@ def convert_date(from_time, full=True, second=True) -> Union[str, int]:
|
|||||||
if from_time.isdigit():
|
if from_time.isdigit():
|
||||||
from_timestamp = int(from_time)
|
from_timestamp = int(from_time)
|
||||||
if full:
|
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:
|
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:
|
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:
|
else:
|
||||||
parts = from_time.split()
|
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:
|
def convert_times(get_time: int, get_type: str = "day") -> str:
|
||||||
get_time = int(get_time)
|
get_time = int(get_time)
|
||||||
if get_time < 0: get_time = 0
|
if get_time < 0:
|
||||||
|
get_time = 0
|
||||||
|
|
||||||
if get_type == "second":
|
if get_type == "second":
|
||||||
get_list = ['секунда', 'секунды', 'секунд']
|
get_list = ["секунда", "секунды", "секунд"]
|
||||||
elif get_type == "minute":
|
elif get_type == "minute":
|
||||||
get_list = ['минута', 'минуты', 'минут']
|
get_list = ["минута", "минуты", "минут"]
|
||||||
elif get_type == "hour":
|
elif get_type == "hour":
|
||||||
get_list = ['час', 'часа', 'часов']
|
get_list = ["час", "часа", "часов"]
|
||||||
elif get_type == "day":
|
elif get_type == "day":
|
||||||
get_list = ['день', 'дня', 'дней']
|
get_list = ["день", "дня", "дней"]
|
||||||
elif get_type == "month":
|
elif get_type == "month":
|
||||||
get_list = ['месяц', 'месяца', 'месяцев']
|
get_list = ["месяц", "месяца", "месяцев"]
|
||||||
else:
|
else:
|
||||||
get_list = ['год', 'года', 'лет']
|
get_list = ["год", "года", "лет"]
|
||||||
|
|
||||||
if get_time % 10 == 1 and get_time % 100 != 11:
|
if get_time % 10 == 1 and get_time % 100 != 11:
|
||||||
count = 0
|
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:
|
def is_bool(value: Union[bool, str, int]) -> bool:
|
||||||
value = str(value).strip().lower()
|
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
|
return True
|
||||||
elif value in ('n', 'no', 'f', 'false', 'off', '0'):
|
elif value in ("n", "no", "f", "false", "off", "0"):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Некорректное bool-значение: {value}")
|
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]
|
str_amount = str_amount[:remains_save]
|
||||||
|
|
||||||
if "." in str(str_amount):
|
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)
|
return str(str_amount)
|
||||||
|
|
||||||
@@ -299,7 +319,8 @@ def is_number(get_number: Union[str, int, float]) -> bool:
|
|||||||
if str(get_number).isdigit():
|
if str(get_number).isdigit():
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
if "," in str(get_number): get_number = str(get_number).replace(",", ".")
|
if "," in str(get_number):
|
||||||
|
get_number = str(get_number).replace(",", ".")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
float(get_number)
|
float(get_number)
|
||||||
@@ -318,3 +339,82 @@ def format_rate(amount: Union[float, int], around: int = 2) -> str:
|
|||||||
response = response.rstrip("0").rstrip(".")
|
response = response.rstrip("0").rstrip(".")
|
||||||
|
|
||||||
return response
|
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)
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ from tgbot.utils.misc.bot_logging import bot_logger
|
|||||||
# Команды для обычных пользователей
|
# Команды для обычных пользователей
|
||||||
user_commands = [
|
user_commands = [
|
||||||
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
||||||
# BotCommand(command="menu", description="🌀 Получение клавиатуры"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Команды для админов
|
# Команды для админов
|
||||||
admin_commands = [
|
admin_commands = [
|
||||||
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
||||||
# BotCommand(command="menu", description="🌀 Получение клавиатуры"),
|
BotCommand(command="admin", description="⚙️ Панель управления"),
|
||||||
BotCommand(command="log", description="🖨 Получить логи"),
|
BotCommand(command="log", description="🖨 Получить логи"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user