diff --git a/app/callbacks/menu.py b/app/callbacks/menu.py
index ae6db89..92ac2ef 100644
--- a/app/callbacks/menu.py
+++ b/app/callbacks/menu.py
@@ -93,8 +93,17 @@ async def travels_callback(
await callback.message.answer(
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()
diff --git a/app/callbacks/profile.py b/app/callbacks/profile.py
index 9ab6cc1..5866616 100644
--- a/app/callbacks/profile.py
+++ b/app/callbacks/profile.py
@@ -152,11 +152,20 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
elif column == "location":
location = value.split(", ")
+ proccessing_message = await message.answer(messages.PROCCESSING)
+
if len(location) != 2:
- await handle_validation_error(
- message,
+ await delete_message_from_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
@@ -169,7 +178,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
value=country,
)
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
@@ -179,7 +199,18 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
country=validated_country,
)
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
@@ -204,6 +235,11 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
"city": state_data["value"][1],
},
)
+
+ try:
+ await proccessing_message.delete()
+ except TelegramBadRequest:
+ pass
else:
data = {state_data["column"]: state_data["value"]}
user.update(data)
diff --git a/app/callbacks/travels.py b/app/callbacks/travels.py
index 27ee397..4a6dd31 100644
--- a/app/callbacks/travels.py
+++ b/app/callbacks/travels.py
@@ -9,6 +9,8 @@ from app import messages
from app.config import Config
from app.filters.user import RegisteredCallback
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
@@ -40,5 +42,32 @@ async def travels_callback(callback: CallbackQuery) -> None:
await callback.message.edit_text(
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),
+ )
diff --git a/app/handlers/start_command.py b/app/handlers/start_command.py
index 02897e7..07dd023 100644
--- a/app/handlers/start_command.py
+++ b/app/handlers/start_command.py
@@ -1,6 +1,7 @@
__all__ = ("router",)
from aiogram import F, Router
+from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import CommandStart, StateFilter
from aiogram.fsm.context import FSMContext
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:
return
+ proccessing_message = await message.answer(messages.PROCCESSING)
+
location = message.text.strip().split(", ")
if len(location) != 2:
- await handle_validation_error(
- message,
+ await delete_message_from_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
@@ -185,7 +195,18 @@ async def location_handler(message: Message, state: FSMContext) -> None:
value=country,
)
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
@@ -195,10 +216,26 @@ async def location_handler(message: Message, state: FSMContext) -> None:
country=validated_country,
)
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
+ try:
+ await proccessing_message.delete()
+ except TelegramBadRequest:
+ pass
+
await delete_message_from_state(state, message.chat.id, message.bot)
await state.update_data(location=[validated_country, validated_city])
diff --git a/app/handlers/travels_command.py b/app/handlers/travels_command.py
index 97eb8c9..daf826d 100644
--- a/app/handlers/travels_command.py
+++ b/app/handlers/travels_command.py
@@ -32,5 +32,10 @@ async def command_help_handler(message: Message) -> None:
await message.answer(
messages.TRAVELS,
- reply_markup=travels_keyboard(travels, page, pages),
+ reply_markup=travels_keyboard(
+ travels,
+ page,
+ pages,
+ user.telegram_id,
+ ),
)
diff --git a/app/keyboards/builders.py b/app/keyboards/builders.py
index 4e93bd0..290a3dc 100644
--- a/app/keyboards/builders.py
+++ b/app/keyboards/builders.py
@@ -16,7 +16,7 @@ def sex_keyboard(choices: str | list):
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()
rows = []
@@ -24,9 +24,14 @@ def travels_keyboard(travels: list, page: int, pages: int):
end_index = min((page + 1) * Config.PAGE_SIZE, len(travels))
for travel in travels[start_index:end_index]:
+ button_text = travel.title
+
+ if travel.author_id == user_id:
+ button_text += " đ"
+
rows.append(
InlineKeyboardButton(
- text=travel.title,
+ text=button_text,
callback_data=f"travel_detail_{travel.id}",
),
)
diff --git a/app/keyboards/travel.py b/app/keyboards/travel.py
new file mode 100644
index 0000000..7a005a7
--- /dev/null
+++ b/app/keyboards/travel.py
@@ -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()
diff --git a/app/messages.py b/app/messages.py
index 7f613e8..15db843 100644
--- a/app/messages.py
+++ b/app/messages.py
@@ -2,7 +2,7 @@
MENU = "Menu:"
-TRAVELS = "đ Travels:"
+TRAVELS = "đ Travels:\nđ - owner"
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
CREATE_TRAVEL = (
"đ§ŗ Let's create new travel!\nEnter /cancel to cancel creating."
@@ -14,9 +14,14 @@ INPUT_TRAVEL_CALLBACK = (
"All right, travel {key} is set to: {value}"
)
INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\nMaximum length: 100 characters"
-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 {title} successfully created! You can now view and edit it in the travels list (/travels command)."
ACTION_CANCELED = "â Action canceled"
+TRAVEL_DETAIL = (
+ "đ Travel detail\n\n"
+ "\tTitle: {title}\n"
+ "\tDescription: {description}\n"
+)
WELCOME_MESSAGE = "Hello, {name}! Welcome to the âī¸ Travel Agent bot! Let's start our journey by filling out some information about you."
WELCOME_AGAIN_MESSAGE = "Hello, {name}! 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:\nRange: 13-120"
INPUT_SEX = "Enter your sex:\nOptions: Male or Female"
INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\nMaximum length: 100 characters"
-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:\nFormat: country, city\nExample: Russia, Moscow"
-INPUT_CALLBACK = "All right, your {key} is set to: {value}"
-VALIDATION_ERROR = "Invalid input. Please try again."
+INPUT_CALLBACK = "â
All right, your {key} is set to: {value}"
+VALIDATION_ERROR = "â Invalid input. Please try again."
CANCEL_CHANGE = "Enter /cancel to cancel change."
PROFILE = (
- "Your profile:\n\n"
+ "đ¤ Your profile:\n\n"
"\tUsername: {username}\n"
"\tAge: {age}\n"
"\tSex: {sex}\n"
@@ -60,3 +65,5 @@ EDIT_USERNAME = "Enter your username:\nAllowed characters: a-z, A-Z, 0-9, _
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\nMaximum length: 100 characters"
PROFILE_UPDATED = "â
Profile updated"
CHANGE_CANCELED = "â Change canceled"
+
+PROCCESSING = "âī¸ Processing..."
diff --git a/app/models/travel.py b/app/models/travel.py
index 08223b2..04bd229 100644
--- a/app/models/travel.py
+++ b/app/models/travel.py
@@ -3,7 +3,7 @@ __all__ = ("Travel", "Location")
import sqlalchemy as sa
from sqlalchemy.orm import relationship, validates
-from app import session
+from app import messages, session
from app.models import Base
from app.models.user import User
@@ -55,6 +55,7 @@ class Travel(Base):
@validates("title")
def validate_title(self, key, value):
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():
raise AssertionError("This title is already taken.")
@@ -70,6 +71,16 @@ class Travel(Base):
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):
__tablename__ = "locations"
diff --git a/app/utils/states.py b/app/utils/states.py
index 0d11572..fcff93a 100644
--- a/app/utils/states.py
+++ b/app/utils/states.py
@@ -56,7 +56,7 @@ async def handle_validation_error(
message.bot,
)
- error_message = await message.answer(str(e))
+ error_message = await message.answer("â " + str(e))
await state.update_data(
error_message_id=error_message.message_id,
)
diff --git a/docker-compose.yml b/docker-compose.yml
index 1499a8e..9949560 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -38,7 +38,7 @@ services:
redis:
condition: service_healthy
environment:
- BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q}
+ BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk}
REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
entrypoint: ["bash", "-c"]
diff --git a/template.env b/template.env
index 788eb21..5db8394 100644
--- a/template.env
+++ b/template.env
@@ -1,6 +1,6 @@
# For app
-BOT_TOKEN = # default: 6943803094:AAEHG-vOP2pNEuxb9rDIhisiQuGLuBIjx1Q
+BOT_TOKEN = # default: 6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk
SQLALCHEMY_DATABASE_URI = # no need to specify if docker is used
REDIS_URL = # no need to specify if docker is used