diff --git a/app/bot.py b/app/bot.py index 3f63623..918fac6 100644 --- a/app/bot.py +++ b/app/bot.py @@ -5,6 +5,7 @@ from typing import Optional from aiogram import Bot, Dispatcher from aiogram.enums import ParseMode +from app.callbacks import profile from app.config import Config from app.handlers import help_command, profile_command, start_command from app.middlewares.throttling import ThrottlingMiddleware @@ -18,9 +19,11 @@ async def main() -> None: bot = Bot(bot_token, parse_mode=ParseMode.HTML) dp.message.middleware(ThrottlingMiddleware(0.5)) + # type: ignore dp.include_routers( start_command.router, profile_command.router, + profile.router, help_command.router, ) diff --git a/app/callbacks/profile.py b/app/callbacks/profile.py new file mode 100644 index 0000000..4b0d969 --- /dev/null +++ b/app/callbacks/profile.py @@ -0,0 +1,171 @@ +# type: ignore +__all__ = () + +from aiogram import F, Router +from aiogram.exceptions import TelegramBadRequest +from aiogram.filters import StateFilter +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message, ReplyKeyboardRemove + +from app import messages, session +from app.filters.user_filter import Registered, RegisteredCallback +from app.keyboards.builders import profile +from app.keyboards.profile import get +from app.models.user import User +from app.utils.states import UserAltering + + +router = Router(name="profile_callback") + + +@router.callback_query( + F.data.startswith("profile_change_"), + StateFilter(None), + RegisteredCallback(), +) +async def profile_change_callback( + callback: CallbackQuery, + state: FSMContext, +) -> None: + if callback.data is None or callback.message is None: + return + + column = callback.data.replace("profile_change_", "") + + if column == "username": + await callback.message.answer(messages.EDIT_USERNAME) + elif column == "age": + await callback.message.answer(messages.INPUT_AGE) + elif column == "bio": + await callback.message.answer(messages.EDIT_BIO) + elif column == "sex": + await callback.message.answer( + messages.INPUT_SEX, + reply_markup=profile(["Male", "Female"]), + ) + elif column == "location": + await callback.message.answer(messages.INPUT_LOCATION) + + await state.update_data( + column=column, + message_id=callback.message.message_id, + ) + await state.set_state(UserAltering.value) + + await callback.answer() + + +@router.message(UserAltering.value, F.text, Registered()) +async def profile_change_entered(message: Message, state: FSMContext) -> None: + column = (await state.get_data()).get("column") + value = message.text.strip() + + if column == "username": + try: + validated_value = User().validate_username( + key="username", + value=value, + ) + except AssertionError as e: + await message.answer(str(e)) + return + + await state.update_data(value=validated_value) + elif column == "age": + try: + validated_age = User().validate_age(key="age", value=value) + except AssertionError as e: + await message.answer(str(e)) + return + + await state.update_data(value=validated_age) + elif column == "bio": + if value == "/skip": + await state.update_data(value=None) + else: + try: + validated_bio = User().validate_bio(key="bio", value=value) + except AssertionError as e: + await message.answer(str(e)) + return + + await state.update_data(value=validated_bio) + elif column == "sex": + value = value.lower() + + if value not in ["male", "female"]: + await message.answer(messages.VALIDATION_ERROR_MESSAGE) + return + + await state.update_data(value=value) + elif column == "location": + location = value.split(", ") + if len(location) != 2: + await message.answer(messages.VALIDATION_ERROR_MESSAGE) + return + + country, city = location + + try: + validated_country = User().validate_country( + key="country", + value=country, + ) + except AssertionError as e: + await message.answer(str(e)) + return + + try: + validated_city = User().validate_city( + city=city, + country=validated_country, + ) + except AssertionError as e: + await message.answer(str(e)) + return + + await state.update_data(value=[validated_country, validated_city]) + + state_data = await state.get_data() + + user = User.get_user_queryset_by_telegram_id(message.from_user.id) + + if isinstance(state_data["value"], list): + user.update( + { + "country": state_data["value"][0], + "city": state_data["value"][1], + }, + ) + else: + data = {state_data["column"]: state_data["value"]} + user.update(data) + + session.commit() + + user = user.first() + session.refresh(user) + + try: + await message.bot.edit_message_text( + messages.PROFILE.format( + username=user.username, + age=user.age, + bio=user.bio if user.bio else messages.NOT_SET, + sex=user.sex.capitalize(), + country=user.country, + city=user.city, + ), + message.chat.id, + state_data["message_id"], + reply_markup=get(), + ) + except TelegramBadRequest: + pass + + await message.answer( + "✅ Profile updated", + reply_markup=ReplyKeyboardRemove(), + ) + + await state.clear() diff --git a/app/messages.py b/app/messages.py index 77e0a3c..0419dd8 100644 --- a/app/messages.py +++ b/app/messages.py @@ -26,3 +26,5 @@ PROFILE = ( "\tCity: {city}" ) NOT_SET = "Not set" +EDIT_USERNAME = "Enter your username:\nAllowed characters: a-z, A-Z, 0-9, _\nLength: 5-20 characters" +EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\nMaximum length: 100 characters" diff --git a/app/models/user.py b/app/models/user.py index af1e7d9..119e88f 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -73,6 +73,10 @@ class User(Base): return normalized_value + @classmethod + def get_user_queryset_by_telegram_id(cls, telegram_id): + return session.query(cls).filter(cls.telegram_id == telegram_id) + @classmethod def get_user_by_telegram_id(cls, telegram_id): return ( diff --git a/app/utils/geo.py b/app/utils/geo.py index 8574f6f..9d7e2a5 100644 --- a/app/utils/geo.py +++ b/app/utils/geo.py @@ -23,9 +23,13 @@ def validate_country(country: str): if not geocode: return False, None - is_loc_country = geocode.raw.get( - "type", None, - ) == "administrative" + is_loc_country = ( + geocode.raw.get( + "type", + None, + ) + == "administrative" + ) if is_loc_country: normalized_country = geocode.raw.get("name", "Invalid country") @@ -55,9 +59,13 @@ def validate_city(city: str, country: str): if not geocode: return False, None - check_in_valid = geocode.raw.get( - "type", None, - ) in valid_list + check_in_valid = ( + geocode.raw.get( + "type", + None, + ) + in valid_list + ) if geocode and check_in_valid: normalized_country = geocode.raw.get("name", "Invalid city") diff --git a/app/utils/states.py b/app/utils/states.py index 6eb8429..65dd129 100644 --- a/app/utils/states.py +++ b/app/utils/states.py @@ -12,4 +12,6 @@ class RegistrationForm(StatesGroup): class UserAltering(StatesGroup): - new_value = State() + message_id = State() + column = State() + value = State()