generated from FOSS/Bot-Template
style: reformat imports, whitespace and line breaks across project
This commit is contained in:
@@ -4,7 +4,12 @@ from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import (
|
||||||
|
AsyncAttrs,
|
||||||
|
AsyncSession,
|
||||||
|
async_sessionmaker,
|
||||||
|
create_async_engine,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
from tgbot.data.config import PATH_DATABASE
|
from tgbot.data.config import PATH_DATABASE
|
||||||
|
|||||||
+18
-14
@@ -13,8 +13,12 @@ from tgbot.utils.const_functions import get_unix
|
|||||||
class UserModel(Base):
|
class UserModel(Base):
|
||||||
__tablename__ = "storage_users"
|
__tablename__ = "storage_users"
|
||||||
|
|
||||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
increment: Mapped[int] = mapped_column(
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
|
Integer, primary_key=True, autoincrement=True
|
||||||
|
)
|
||||||
|
user_id: Mapped[int] = mapped_column(
|
||||||
|
BigInteger, nullable=False, unique=True, index=True
|
||||||
|
)
|
||||||
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||||
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||||
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||||
@@ -35,12 +39,12 @@ class UsersRepository(BaseRepository[UserModel]):
|
|||||||
|
|
||||||
# Для совместимости add ведет себя как upsert
|
# Для совместимости add ведет себя как upsert
|
||||||
async def add(
|
async def add(
|
||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
user_login: str,
|
user_login: str,
|
||||||
user_name: str,
|
user_name: str,
|
||||||
user_surname: str,
|
user_surname: str,
|
||||||
user_fullname: str,
|
user_fullname: str,
|
||||||
) -> UserModel:
|
) -> UserModel:
|
||||||
return await self.upsert(
|
return await self.upsert(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -52,12 +56,12 @@ class UsersRepository(BaseRepository[UserModel]):
|
|||||||
|
|
||||||
# Создание пользователя или обновление его данных по user_id
|
# Создание пользователя или обновление его данных по user_id
|
||||||
async def upsert(
|
async def upsert(
|
||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
user_login: str,
|
user_login: str,
|
||||||
user_name: str,
|
user_name: str,
|
||||||
user_surname: str,
|
user_surname: str,
|
||||||
user_fullname: str,
|
user_fullname: str,
|
||||||
) -> UserModel:
|
) -> UserModel:
|
||||||
statement = insert(UserModel).values(
|
statement = insert(UserModel).values(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ class BaseRepository(Generic[ModelTranslator]):
|
|||||||
|
|
||||||
async with session_scope() as session:
|
async with session_scope() as session:
|
||||||
await session.execute(
|
await session.execute(
|
||||||
sqlalchemy_update(model)
|
sqlalchemy_update(model).filter_by(**filters).values(**kwargs)
|
||||||
.filter_by(**filters)
|
|
||||||
.values(**kwargs)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +99,8 @@ async def prepare_database() -> None:
|
|||||||
try:
|
try:
|
||||||
await SettingsRepository().ensure_default()
|
await SettingsRepository().ensure_default()
|
||||||
except SQLAlchemyError as error:
|
except SQLAlchemyError as error:
|
||||||
raise RuntimeError("База данных не готова. Запусти миграции командой: python migrate.py") from error
|
raise RuntimeError(
|
||||||
|
"База данных не готова. Запусти миграции командой: python migrate.py"
|
||||||
|
) from error
|
||||||
|
|
||||||
bot_logger.info("База данных готова")
|
bot_logger.info("База данных готова")
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ class ThrottlingMiddleware(BaseMiddleware):
|
|||||||
self.message_users = TTLCache(maxsize=10_000, ttl=600)
|
self.message_users = TTLCache(maxsize=10_000, ttl=600)
|
||||||
self.callback_users = TTLCache(maxsize=10_000, ttl=600)
|
self.callback_users = TTLCache(maxsize=10_000, ttl=600)
|
||||||
|
|
||||||
async def __call__(self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, data):
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data,
|
||||||
|
):
|
||||||
# Если юзер спамит, постепенно увеличиваем паузу
|
# Если юзер спамит, постепенно увеличиваем паузу
|
||||||
this_user: Optional[User] = data.get("event_from_user")
|
this_user: Optional[User] = data.get("event_from_user")
|
||||||
|
|
||||||
@@ -38,28 +43,31 @@ class ThrottlingMiddleware(BaseMiddleware):
|
|||||||
|
|
||||||
if user_key not in bucket:
|
if user_key not in bucket:
|
||||||
bucket[user_key] = {
|
bucket[user_key] = {
|
||||||
'last_throttled': now_time,
|
"last_throttled": now_time,
|
||||||
'count_throttled': 0,
|
"count_throttled": 0,
|
||||||
'now_rate': rate,
|
"now_rate": rate,
|
||||||
}
|
}
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
else:
|
else:
|
||||||
if now_time - bucket[user_key]['last_throttled'] >= bucket[user_key]['now_rate']:
|
if (
|
||||||
|
now_time - bucket[user_key]["last_throttled"]
|
||||||
|
>= bucket[user_key]["now_rate"]
|
||||||
|
):
|
||||||
bucket.pop(user_key)
|
bucket.pop(user_key)
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
else:
|
else:
|
||||||
bucket[user_key]['last_throttled'] = now_time
|
bucket[user_key]["last_throttled"] = now_time
|
||||||
bucket[user_key]['count_throttled'] += 1
|
bucket[user_key]["count_throttled"] += 1
|
||||||
|
|
||||||
if bucket[user_key]['count_throttled'] == 1:
|
if bucket[user_key]["count_throttled"] == 1:
|
||||||
bucket[user_key]['now_rate'] = rate + 2
|
bucket[user_key]["now_rate"] = rate + 2
|
||||||
await self._warn_user(event)
|
await self._warn_user(event)
|
||||||
elif bucket[user_key]['count_throttled'] == 2:
|
elif bucket[user_key]["count_throttled"] == 2:
|
||||||
bucket[user_key]['now_rate'] = rate + 3
|
bucket[user_key]["now_rate"] = rate + 3
|
||||||
else:
|
else:
|
||||||
bucket[user_key]['now_rate'] = rate + 5
|
bucket[user_key]["now_rate"] = rate + 5
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,15 @@ class ExistsUserMiddleware(BaseMiddleware):
|
|||||||
user_surname = clear_html(this_user.last_name)
|
user_surname = clear_html(this_user.last_name)
|
||||||
user_fullname = clear_html(this_user.first_name)
|
user_fullname = clear_html(this_user.first_name)
|
||||||
|
|
||||||
if user_name is None: user_name = ""
|
if user_name is None:
|
||||||
if user_surname is None: user_surname = ""
|
user_name = ""
|
||||||
if user_fullname is None: user_fullname = ""
|
if user_surname is None:
|
||||||
|
user_surname = ""
|
||||||
|
if user_fullname is None:
|
||||||
|
user_fullname = ""
|
||||||
|
|
||||||
if len(user_surname) >= 1: user_fullname += f" {user_surname}"
|
if len(user_surname) >= 1:
|
||||||
|
user_fullname += f" {user_surname}"
|
||||||
|
|
||||||
user_data = (
|
user_data = (
|
||||||
user_login.lower(),
|
user_login.lower(),
|
||||||
@@ -50,6 +54,6 @@ class ExistsUserMiddleware(BaseMiddleware):
|
|||||||
else:
|
else:
|
||||||
user = cached_user["user"]
|
user = cached_user["user"]
|
||||||
|
|
||||||
data['User'] = user
|
data["User"] = user
|
||||||
|
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ def register_all_routers(dp: Dispatcher):
|
|||||||
|
|
||||||
user_menu.router.message.filter(IsPrivate())
|
user_menu.router.message.filter(IsPrivate())
|
||||||
user_menu.router.callback_query.filter(IsPrivate())
|
user_menu.router.callback_query.filter(IsPrivate())
|
||||||
|
|
||||||
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,33 +22,20 @@ SERVICE_LOG_FILES = (
|
|||||||
|
|
||||||
|
|
||||||
# Кнопка админского меню
|
# Кнопка админского меню
|
||||||
@router.message(F.text == 'Админ-меню')
|
@router.message(F.text == "Админ-меню")
|
||||||
async def admin_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def admin_button_inline(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(
|
await message.answer("Инлайн-клавиатура админа", reply_markup=admin_finl())
|
||||||
"Инлайн-клавиатура админа",
|
|
||||||
reply_markup=admin_finl()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Колбэк для демо-действия админа
|
|
||||||
@router.callback_query(F.data == 'admin_inline_x')
|
|
||||||
async def admin_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
|
||||||
await call.answer("Админское действие выполнено")
|
|
||||||
|
|
||||||
|
|
||||||
# Колбэк с параметром из админской кнопки
|
|
||||||
@router.callback_query(F.data.startswith('admin_inline:'))
|
|
||||||
async def admin_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
|
||||||
get_data = call.data.split(":")[1]
|
|
||||||
|
|
||||||
await call.answer(f"Выбран админский раздел: {get_data}", True)
|
|
||||||
|
|
||||||
|
|
||||||
# Отправка файла базы админам
|
# Отправка файла базы админам
|
||||||
@router.message(Command(commands=['db', 'database']))
|
@router.message(Command(commands=["db", "database"]))
|
||||||
async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def admin_database(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
if not BOT_DATABASE_EXPORT:
|
if not BOT_DATABASE_EXPORT:
|
||||||
@@ -66,8 +53,10 @@ async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS,
|
|||||||
|
|
||||||
|
|
||||||
# Отправка логов админам
|
# Отправка логов админам
|
||||||
@router.message(Command(commands=['log', 'logs']))
|
@router.message(Command(commands=["log", "logs"]))
|
||||||
async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def admin_log(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
log_files = []
|
log_files = []
|
||||||
@@ -95,8 +84,12 @@ async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User
|
|||||||
|
|
||||||
|
|
||||||
# Очистка файлов логов
|
# Очистка файлов логов
|
||||||
@router.message(Command(commands=['clear_log', 'clear_logs', 'log_clear', 'logs_clear']))
|
@router.message(
|
||||||
async def admin_logs_clear(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
Command(commands=["clear_log", "clear_logs", "log_clear", "logs_clear"])
|
||||||
|
)
|
||||||
|
async def admin_logs_clear(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
log_files = [Path(PATH_LOGS), *SERVICE_LOG_FILES]
|
log_files = [Path(PATH_LOGS), *SERVICE_LOG_FILES]
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ router = Router(name=__name__)
|
|||||||
class MessageNotModifiedHandler(ErrorHandler):
|
class MessageNotModifiedHandler(ErrorHandler):
|
||||||
async def handle(self):
|
async def handle(self):
|
||||||
if "message is not modified" in self.exception_message.lower():
|
if "message is not modified" in self.exception_message.lower():
|
||||||
bot_logger.debug("Telegram отклонил повторное редактирование сообщения без изменений")
|
bot_logger.debug(
|
||||||
|
"Telegram отклонил повторное редактирование сообщения без изменений"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
raise self.event
|
raise self.event
|
||||||
|
|||||||
@@ -10,27 +10,34 @@ router = Router(name=__name__)
|
|||||||
|
|
||||||
|
|
||||||
# Колбэк для удаления текущего сообщения
|
# Колбэк для удаления текущего сообщения
|
||||||
@router.callback_query(F.data == 'close_this')
|
@router.callback_query(F.data == "close_this")
|
||||||
async def main_callback_close(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers):
|
async def main_callback_close(
|
||||||
|
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers
|
||||||
|
):
|
||||||
await del_message(call.message)
|
await del_message(call.message)
|
||||||
|
|
||||||
|
|
||||||
# Колбэк-заглушка для пустых кнопок
|
# Колбэк-заглушка для пустых кнопок
|
||||||
@router.callback_query(F.data == '...')
|
@router.callback_query(F.data == "...")
|
||||||
async def main_callback_answer(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers):
|
async def main_callback_answer(
|
||||||
|
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers
|
||||||
|
):
|
||||||
await call.answer(cache_time=30)
|
await call.answer(cache_time=30)
|
||||||
|
|
||||||
|
|
||||||
# Ответ на колбэк, который никто не обработал
|
# Ответ на колбэк, который никто не обработал
|
||||||
@router.callback_query()
|
@router.callback_query()
|
||||||
async def main_callback_missed(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers):
|
async def main_callback_missed(
|
||||||
|
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers
|
||||||
|
):
|
||||||
await call.answer(f"❗️ Неизвестный колбэк: {call.data}", True)
|
await call.answer(f"❗️ Неизвестный колбэк: {call.data}", True)
|
||||||
|
|
||||||
|
|
||||||
# Ответ на неизвестные сообщения
|
# Ответ на неизвестные сообщения
|
||||||
@router.message()
|
@router.message()
|
||||||
async def main_message_missed(message: Message, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers):
|
async def main_message_missed(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: ModelUsers
|
||||||
|
):
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"♦️ Неизвестная команда\n"
|
"♦️ Неизвестная команда\n" "♦️ Введите /start",
|
||||||
"♦️ Введите /start",
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ 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"]))
|
||||||
async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def main_start(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
|
|||||||
@@ -12,36 +12,23 @@ router = Router(name=__name__)
|
|||||||
|
|
||||||
|
|
||||||
# Кнопка пользовательского меню
|
# Кнопка пользовательского меню
|
||||||
@router.message(F.text == 'Пользовательское меню')
|
# @router.message(F.text == "Пользовательское меню")
|
||||||
async def user_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def user_button_inline(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(
|
await message.answer("Инлайн-клавиатура пользователя", reply_markup=user_finl())
|
||||||
"Инлайн-клавиатура пользователя",
|
|
||||||
reply_markup=user_finl()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Команда возврата в меню
|
# Команда возврата в меню
|
||||||
@router.message(Command(commands="menu"))
|
# @router.message(Command(commands="menu"))
|
||||||
async def user_command_menu(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
async def user_command_menu(
|
||||||
|
message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel
|
||||||
|
):
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"Команда /menu открывает главное меню",
|
"Команда /menu открывает главное меню",
|
||||||
reply_markup=menu_frep(message.from_user.id),
|
reply_markup=menu_frep(message.from_user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Колбэк для демо-действия
|
|
||||||
@router.callback_query(F.data == 'user_inline_x')
|
|
||||||
async def user_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
|
||||||
await call.answer("Действие выполнено")
|
|
||||||
|
|
||||||
|
|
||||||
# Колбэк с параметром из пользовательской кнопки
|
|
||||||
@router.callback_query(F.data.startswith('user_inline:'))
|
|
||||||
async def user_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel):
|
|
||||||
get_data = call.data.split(":")[1]
|
|
||||||
|
|
||||||
await call.answer(f"Выбран раздел: {get_data}", True)
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ from typing import Optional
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
# Пример использования в обработчике
|
# Пример использования в обработчике
|
||||||
# session = await arSession.get_session()
|
# session = await arSession.get_session()
|
||||||
# response = await session.get(...)
|
# response = await session.get(...)
|
||||||
# response = await session.post(...)
|
# response = await session.post(...)
|
||||||
|
|
||||||
|
|
||||||
# Асинхронная сессия для запросов
|
# Асинхронная сессия для запросов
|
||||||
class AsyncRequestSession:
|
class AsyncRequestSession:
|
||||||
def __init__(self, timeout: int = 30) -> None:
|
def __init__(self, timeout: int = 30) -> None:
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ 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="🌀 Получение клавиатуры"),
|
# BotCommand(command="menu", description="🌀 Получение клавиатуры"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Команды для админов
|
# Команды для админов
|
||||||
admin_commands = [
|
admin_commands = [
|
||||||
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
BotCommand(command="start", description="♻️ Перезапуск бота"),
|
||||||
BotCommand(command="menu", description="🌀 Получение клавиатуры"),
|
# BotCommand(command="menu", description="🌀 Получение клавиатуры"),
|
||||||
BotCommand(command="log", description="🖨 Получить логи"),
|
BotCommand(command="log", description="🖨 Получить логи"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ async def set_commands(bot: Bot):
|
|||||||
|
|
||||||
for admin in get_admins():
|
for admin in get_admins():
|
||||||
try:
|
try:
|
||||||
await bot.set_my_commands(admin_commands, scope=BotCommandScopeChat(chat_id=admin))
|
await bot.set_my_commands(
|
||||||
|
admin_commands, scope=BotCommandScopeChat(chat_id=admin)
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
bot_logger.warning("Не удалось обновить команды для админа %s", admin, exc_info=True)
|
bot_logger.warning(
|
||||||
|
"Не удалось обновить команды для админа %s", admin, exc_info=True
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user