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()