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, _Maximum 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