You've already forked Travel-Agent
feat: Added travel detail, added proccessing message when changing/setting user location, improvements in messages text
This commit is contained in:
+11
-2
@@ -93,8 +93,17 @@ async def travels_callback(
|
|||||||
|
|
||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
messages.TRAVELS,
|
messages.TRAVELS,
|
||||||
reply_markup=travels_keyboard(travels, page, pages),
|
reply_markup=travels_keyboard(
|
||||||
|
travels,
|
||||||
|
page,
|
||||||
|
pages,
|
||||||
|
user.telegram_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await callback.message.delete()
|
try:
|
||||||
|
await callback.message.delete()
|
||||||
|
except TelegramBadRequest:
|
||||||
|
pass
|
||||||
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|||||||
@@ -152,11 +152,20 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
|||||||
elif column == "location":
|
elif column == "location":
|
||||||
location = value.split(", ")
|
location = value.split(", ")
|
||||||
|
|
||||||
|
proccessing_message = await message.answer(messages.PROCCESSING)
|
||||||
|
|
||||||
if len(location) != 2:
|
if len(location) != 2:
|
||||||
await handle_validation_error(
|
await delete_message_from_state(
|
||||||
message,
|
|
||||||
state,
|
state,
|
||||||
messages.VALIDATION_ERROR,
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text(messages.VALIDATION_ERROR)
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -169,7 +178,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
|||||||
value=country,
|
value=country,
|
||||||
)
|
)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
await handle_validation_error(message, state, e)
|
await delete_message_from_state(
|
||||||
|
state,
|
||||||
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text("❌ " + str(e))
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -179,7 +199,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
|||||||
country=validated_country,
|
country=validated_country,
|
||||||
)
|
)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
await handle_validation_error(message, state, e)
|
await delete_message_from_state(
|
||||||
|
state,
|
||||||
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text("❌ " + str(e))
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -204,6 +235,11 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
|||||||
"city": state_data["value"][1],
|
"city": state_data["value"][1],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await proccessing_message.delete()
|
||||||
|
except TelegramBadRequest:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
data = {state_data["column"]: state_data["value"]}
|
data = {state_data["column"]: state_data["value"]}
|
||||||
user.update(data)
|
user.update(data)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from app import messages
|
|||||||
from app.config import Config
|
from app.config import Config
|
||||||
from app.filters.user import RegisteredCallback
|
from app.filters.user import RegisteredCallback
|
||||||
from app.keyboards.builders import travels_keyboard
|
from app.keyboards.builders import travels_keyboard
|
||||||
|
from app.keyboards.travel import get
|
||||||
|
from app.models.travel import Travel
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
@@ -40,5 +42,32 @@ async def travels_callback(callback: CallbackQuery) -> None:
|
|||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
messages.TRAVELS,
|
messages.TRAVELS,
|
||||||
reply_markup=travels_keyboard(travels, page, pages),
|
reply_markup=travels_keyboard(
|
||||||
|
travels,
|
||||||
|
page,
|
||||||
|
pages,
|
||||||
|
user.telegram_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(
|
||||||
|
F.data.startswith("travel_detail"),
|
||||||
|
RegisteredCallback(),
|
||||||
|
StateFilter(None),
|
||||||
|
)
|
||||||
|
async def travel_detail_callback(callback: CallbackQuery) -> None:
|
||||||
|
if callback.data is None or not isinstance(callback.message, Message):
|
||||||
|
return
|
||||||
|
|
||||||
|
travel_id = int(callback.data.replace("travel_detail_", ""))
|
||||||
|
|
||||||
|
travel = Travel().get_travel_by_id(travel_id)
|
||||||
|
|
||||||
|
if not travel:
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
travel.get_travel_text(),
|
||||||
|
reply_markup=get(travel_id),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
__all__ = ("router",)
|
__all__ = ("router",)
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
from aiogram.filters import CommandStart, StateFilter
|
from aiogram.filters import CommandStart, StateFilter
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import Message, ReplyKeyboardRemove
|
from aiogram.types import Message, ReplyKeyboardRemove
|
||||||
@@ -166,13 +167,22 @@ async def location_handler(message: Message, state: FSMContext) -> None:
|
|||||||
if message.text is None or message.from_user is None:
|
if message.text is None or message.from_user is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
proccessing_message = await message.answer(messages.PROCCESSING)
|
||||||
|
|
||||||
location = message.text.strip().split(", ")
|
location = message.text.strip().split(", ")
|
||||||
|
|
||||||
if len(location) != 2:
|
if len(location) != 2:
|
||||||
await handle_validation_error(
|
await delete_message_from_state(
|
||||||
message,
|
|
||||||
state,
|
state,
|
||||||
messages.VALIDATION_ERROR,
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text(messages.VALIDATION_ERROR)
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -185,7 +195,18 @@ async def location_handler(message: Message, state: FSMContext) -> None:
|
|||||||
value=country,
|
value=country,
|
||||||
)
|
)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
await handle_validation_error(message, state, e)
|
await delete_message_from_state(
|
||||||
|
state,
|
||||||
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text("❌ " + str(e))
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -195,10 +216,26 @@ async def location_handler(message: Message, state: FSMContext) -> None:
|
|||||||
country=validated_country,
|
country=validated_country,
|
||||||
)
|
)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
await handle_validation_error(message, state, e)
|
await delete_message_from_state(
|
||||||
|
state,
|
||||||
|
message.chat.id,
|
||||||
|
message.bot,
|
||||||
|
)
|
||||||
|
await proccessing_message.edit_text("❌ " + str(e))
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
error_message = proccessing_message
|
||||||
|
await state.update_data(
|
||||||
|
error_message_id=error_message.message_id,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await proccessing_message.delete()
|
||||||
|
except TelegramBadRequest:
|
||||||
|
pass
|
||||||
|
|
||||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||||
|
|
||||||
await state.update_data(location=[validated_country, validated_city])
|
await state.update_data(location=[validated_country, validated_city])
|
||||||
|
|||||||
@@ -32,5 +32,10 @@ async def command_help_handler(message: Message) -> None:
|
|||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
messages.TRAVELS,
|
messages.TRAVELS,
|
||||||
reply_markup=travels_keyboard(travels, page, pages),
|
reply_markup=travels_keyboard(
|
||||||
|
travels,
|
||||||
|
page,
|
||||||
|
pages,
|
||||||
|
user.telegram_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ def sex_keyboard(choices: str | list):
|
|||||||
return builder.as_markup(resize_keyboard=True)
|
return builder.as_markup(resize_keyboard=True)
|
||||||
|
|
||||||
|
|
||||||
def travels_keyboard(travels: list, page: int, pages: int):
|
def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
@@ -24,9 +24,14 @@ def travels_keyboard(travels: list, page: int, pages: int):
|
|||||||
end_index = min((page + 1) * Config.PAGE_SIZE, len(travels))
|
end_index = min((page + 1) * Config.PAGE_SIZE, len(travels))
|
||||||
|
|
||||||
for travel in travels[start_index:end_index]:
|
for travel in travels[start_index:end_index]:
|
||||||
|
button_text = travel.title
|
||||||
|
|
||||||
|
if travel.author_id == user_id:
|
||||||
|
button_text += " 👑"
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=travel.title,
|
text=button_text,
|
||||||
callback_data=f"travel_detail_{travel.id}",
|
callback_data=f"travel_detail_{travel.id}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
__all__ = ("get",)
|
||||||
|
|
||||||
|
from aiogram import types
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
|
||||||
|
def get(travel_id: int):
|
||||||
|
builder = InlineKeyboardBuilder()
|
||||||
|
|
||||||
|
builder.row(
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="📝 Change title",
|
||||||
|
callback_data=f"travel_change_{travel_id}_title",
|
||||||
|
),
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="ℹ️ Change description",
|
||||||
|
callback_data=f"travel_change_{travel_id}_description",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="➕ Add location",
|
||||||
|
callback_data=f"travel_add_{travel_id}_location",
|
||||||
|
),
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="➕ Add user",
|
||||||
|
callback_data=f"travel_add_{travel_id}_user",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
builder.row(
|
||||||
|
types.InlineKeyboardButton(
|
||||||
|
text="⬅️",
|
||||||
|
callback_data="menu_travels",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return builder.as_markup()
|
||||||
+13
-6
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
MENU = "<b>Menu:</b>"
|
MENU = "<b>Menu:</b>"
|
||||||
|
|
||||||
TRAVELS = "📃 <b>Travels:</b>"
|
TRAVELS = "📃 <b>Travels:</b>\n<i>👑 - owner</i>"
|
||||||
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
|
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
|
||||||
CREATE_TRAVEL = (
|
CREATE_TRAVEL = (
|
||||||
"🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>"
|
"🧳 Let's create new travel!\n<i>Enter /cancel to cancel creating.</i>"
|
||||||
@@ -14,9 +14,14 @@ INPUT_TRAVEL_CALLBACK = (
|
|||||||
"All right, travel <b>{key}</b> is set to: <b>{value}</b>"
|
"All right, travel <b>{key}</b> is set to: <b>{value}</b>"
|
||||||
)
|
)
|
||||||
INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
|
INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
|
||||||
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "Sure. You can always fill it later."
|
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "✅ Sure. You can always fill it later."
|
||||||
TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)."
|
TRAVEL_CREATED = "Travel <b>{title}</b> successfully created! You can now view and edit it in the travels list (/travels command)."
|
||||||
ACTION_CANCELED = "❌ Action canceled"
|
ACTION_CANCELED = "❌ Action canceled"
|
||||||
|
TRAVEL_DETAIL = (
|
||||||
|
"📝 <b>Travel detail</b>\n\n"
|
||||||
|
"\tTitle: <b>{title}</b>\n"
|
||||||
|
"\tDescription: <b>{description}</b>\n"
|
||||||
|
)
|
||||||
|
|
||||||
WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you."
|
WELCOME_MESSAGE = "Hello, <b>{name}</b>! Welcome to the ✈️ Travel Agent bot! Let's start our journey by filling out some information about you."
|
||||||
WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance."
|
WELCOME_AGAIN_MESSAGE = "Hello, <b>{name}</b>! Welcome back to the ✈️ Travel Agent bot! If you get lost, you can always call the /help command for assistance."
|
||||||
@@ -39,14 +44,14 @@ INPUT_USERNAME = "Enter your username (this will be used to interact with other
|
|||||||
INPUT_AGE = "Enter your age:\n<i>Range: 13-120</i>"
|
INPUT_AGE = "Enter your age:\n<i>Range: 13-120</i>"
|
||||||
INPUT_SEX = "Enter your sex:\n<i>Options: Male or Female</i>"
|
INPUT_SEX = "Enter your sex:\n<i>Options: Male or Female</i>"
|
||||||
INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
|
INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\n<i>Maximum length: 100 characters</i>"
|
||||||
INPUT_BIO_SKIPPED = "Sure. You can always fill it later."
|
INPUT_BIO_SKIPPED = "✅ Sure. You can always fill it later."
|
||||||
INPUT_LOCATION = "Enter your location in this format:\n<i>Format: country, city</i>\n<i>Example: Russia, Moscow</i>"
|
INPUT_LOCATION = "Enter your location in this format:\n<i>Format: country, city</i>\n<i>Example: Russia, Moscow</i>"
|
||||||
INPUT_CALLBACK = "All right, your <b>{key}</b> is set to: <b>{value}</b>"
|
INPUT_CALLBACK = "✅ All right, your <b>{key}</b> is set to: <b>{value}</b>"
|
||||||
VALIDATION_ERROR = "Invalid input. Please try again."
|
VALIDATION_ERROR = "❌ Invalid input. Please try again."
|
||||||
CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>"
|
CANCEL_CHANGE = "<i>Enter /cancel to cancel change.</i>"
|
||||||
|
|
||||||
PROFILE = (
|
PROFILE = (
|
||||||
"<b>Your profile:</b>\n\n"
|
"<b>👤 Your profile:</b>\n\n"
|
||||||
"\tUsername: <b>{username}</b>\n"
|
"\tUsername: <b>{username}</b>\n"
|
||||||
"\tAge: <b>{age}</b>\n"
|
"\tAge: <b>{age}</b>\n"
|
||||||
"\tSex: <b>{sex}</b>\n"
|
"\tSex: <b>{sex}</b>\n"
|
||||||
@@ -60,3 +65,5 @@ EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</
|
|||||||
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>"
|
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>"
|
||||||
PROFILE_UPDATED = "✅ Profile updated"
|
PROFILE_UPDATED = "✅ Profile updated"
|
||||||
CHANGE_CANCELED = "❌ Change canceled"
|
CHANGE_CANCELED = "❌ Change canceled"
|
||||||
|
|
||||||
|
PROCCESSING = "⌛️ Processing..."
|
||||||
|
|||||||
+12
-1
@@ -3,7 +3,7 @@ __all__ = ("Travel", "Location")
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.orm import relationship, validates
|
from sqlalchemy.orm import relationship, validates
|
||||||
|
|
||||||
from app import session
|
from app import messages, session
|
||||||
from app.models import Base
|
from app.models import Base
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ class Travel(Base):
|
|||||||
@validates("title")
|
@validates("title")
|
||||||
def validate_title(self, key, value):
|
def validate_title(self, key, value):
|
||||||
assert len(value) <= 30, "Title must be 30 characters or fewer."
|
assert len(value) <= 30, "Title must be 30 characters or fewer."
|
||||||
|
assert "👑" not in value, "👑 is not allowed symbol."
|
||||||
|
|
||||||
if session.query(Travel).filter(Travel.title == value).first():
|
if session.query(Travel).filter(Travel.title == value).first():
|
||||||
raise AssertionError("This title is already taken.")
|
raise AssertionError("This title is already taken.")
|
||||||
@@ -70,6 +71,16 @@ class Travel(Base):
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_travel_text(self):
|
||||||
|
return messages.TRAVEL_DETAIL.format(
|
||||||
|
title=self.title,
|
||||||
|
description=self.description,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_travel_by_id(cls, travel_id):
|
||||||
|
return session.query(Travel).filter(Travel.id == travel_id).first()
|
||||||
|
|
||||||
|
|
||||||
class Location(Base):
|
class Location(Base):
|
||||||
__tablename__ = "locations"
|
__tablename__ = "locations"
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ async def handle_validation_error(
|
|||||||
message.bot,
|
message.bot,
|
||||||
)
|
)
|
||||||
|
|
||||||
error_message = await message.answer(str(e))
|
error_message = await message.answer("❌ " + str(e))
|
||||||
await state.update_data(
|
await state.update_data(
|
||||||
error_message_id=error_message.message_id,
|
error_message_id=error_message.message_id,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q}
|
BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk}
|
||||||
REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
|
REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
|
||||||
SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
|
SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
|
||||||
entrypoint: ["bash", "-c"]
|
entrypoint: ["bash", "-c"]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# For app
|
# For app
|
||||||
|
|
||||||
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q
|
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk
|
||||||
SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used
|
SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used
|
||||||
REDIS_URL = <redis_url> # no need to specify if docker is used
|
REDIS_URL = <redis_url> # no need to specify if docker is used
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user