From d520124d9722dd15121269e0e694eee347e29452 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Fri, 29 Nov 2024 17:20:18 +0000 Subject: [PATCH 1/3] add telegram create notification form --- apps/web_app/telegram/handlers/__init__.py | 2 + .../telegram/handlers/create_notification.py | 105 ++++++++++++++++++ apps/web_app/telegram/handlers/utils/kb.py | 5 + 3 files changed, 112 insertions(+) create mode 100644 apps/web_app/telegram/handlers/create_notification.py diff --git a/apps/web_app/telegram/handlers/__init__.py b/apps/web_app/telegram/handlers/__init__.py index 138f4f72..f0e0b27c 100644 --- a/apps/web_app/telegram/handlers/__init__.py +++ b/apps/web_app/telegram/handlers/__init__.py @@ -2,8 +2,10 @@ from .command import cmd_router from .menu import menu_router +from .create_notification import create_notification_router # Create the main router to simplify imports index_router = Router() index_router.include_router(cmd_router) index_router.include_router(menu_router) +index_router.include_router(create_notification_router) diff --git a/apps/web_app/telegram/handlers/create_notification.py b/apps/web_app/telegram/handlers/create_notification.py new file mode 100644 index 00000000..babf16ba --- /dev/null +++ b/apps/web_app/telegram/handlers/create_notification.py @@ -0,0 +1,105 @@ +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from database.models import NotificationData +from database.schemas import NotificationForm +from telegram.crud import TelegramCrud +from utils.values import ProtocolIDs +from fastapi import status + +create_notification_router = Router() + +class NotificationFormStates(StatesGroup): + wallet_id = State() + health_ratio_level = State() + protocol_id = State() + +@create_notification_router.callback_query(F.data == "create_subscription") +async def start_form(callback: types.CallbackQuery, state: FSMContext): + await state.set_state(NotificationFormStates.wallet_id) + await callback.message.edit_text( + "Please enter your wallet ID:", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ]) + ) + +@create_notification_router.message(NotificationFormStates.wallet_id) +async def process_wallet_id(message: types.Message, state: FSMContext): + await state.update_data(wallet_id=message.text) + await state.set_state(NotificationFormStates.health_ratio_level) + await message.answer( + "Please enter your health ratio level (between 0 and 10):", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ]) + ) + +@create_notification_router.message(NotificationFormStates.health_ratio_level) +async def process_health_ratio(message: types.Message, state: FSMContext): + try: + health_ratio = float(message.text) + if not (0 <= health_ratio <= 10): + raise ValueError + + await state.update_data(health_ratio_level=health_ratio) + await state.set_state(NotificationFormStates.protocol_id) + + # Create protocol selection buttons + protocol_buttons = [] + for protocol in ProtocolIDs: + protocol_buttons.append([ + types.InlineKeyboardButton( + text=protocol.value, + callback_data=f"protocol_{protocol.value}" + ) + ]) + protocol_buttons.append([ + types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form") + ]) + + await message.answer( + "Please select your protocol:", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=protocol_buttons) + ) + except ValueError: + await message.answer( + "Please enter a valid number between 0 and 10.", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ]) + ) + +@create_notification_router.callback_query(F.data.startswith("protocol_")) +async def process_protocol(callback: types.CallbackQuery, state: FSMContext, crud: TelegramCrud): + protocol_id = callback.data.replace("protocol_", "") + form_data = await state.get_data() + + notification = NotificationForm( + wallet_id=form_data["wallet_id"], + health_ratio_level=form_data["health_ratio_level"], + protocol_id=protocol_id, + telegram_id=str(callback.from_user.id), + email="" + ) + + subscription = NotificationData(**notification.model_dump()) + subscription_id = crud.write_to_db(obj=subscription) + + await state.clear() + await callback.message.edit_text( + "Subscription created successfully!", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] + ]) + ) + +@create_notification_router.callback_query(F.data == "cancel_form") +async def cancel_form(callback: types.CallbackQuery, state: FSMContext): + await state.clear() + await callback.message.edit_text( + "Form cancelled.", + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] + ]) + ) \ No newline at end of file diff --git a/apps/web_app/telegram/handlers/utils/kb.py b/apps/web_app/telegram/handlers/utils/kb.py index 5315d1d4..610dd6df 100644 --- a/apps/web_app/telegram/handlers/utils/kb.py +++ b/apps/web_app/telegram/handlers/utils/kb.py @@ -69,6 +69,11 @@ def menu(): text="Shows notifications", callback_data="show_notifications" ), ], + [ + InlineKeyboardButton( + text="Create subscription", callback_data="create_subscription" + ) + ], [ InlineKeyboardButton( text="Unsubscribe all", callback_data="all_unsubscribe" From 9aa9acefe45a375e979d88fe3425850c453f17b9 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Fri, 29 Nov 2024 19:11:13 +0000 Subject: [PATCH 2/3] fix error with telegram form --- apps/web_app/.env.dev | 1 - apps/web_app/database/models.py | 2 +- .../b3179b2fff8b_ip_address_nullable_true.py | 42 +++++++ apps/web_app/telegram/__main__.py | 2 +- apps/web_app/telegram/crud.py | 5 + .../telegram/handlers/create_notification.py | 109 +++++++++++------- 6 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 apps/web_app/migrations/versions/b3179b2fff8b_ip_address_nullable_true.py diff --git a/apps/web_app/.env.dev b/apps/web_app/.env.dev index 598111df..ca96c99d 100644 --- a/apps/web_app/.env.dev +++ b/apps/web_app/.env.dev @@ -1,6 +1,5 @@ DERISK_API_URL=# -ENV=dev # PostgreSQL DB_USER=postgres DB_PASSWORD=password diff --git a/apps/web_app/database/models.py b/apps/web_app/database/models.py index 6754d1dc..fccdeb7f 100644 --- a/apps/web_app/database/models.py +++ b/apps/web_app/database/models.py @@ -35,7 +35,7 @@ class NotificationData(Base): email = Column(String, index=True, nullable=True) wallet_id = Column(String, nullable=False) telegram_id = Column(String, unique=False, nullable=False) - ip_address = Column(IPAddressType, nullable=False) + ip_address = Column(IPAddressType, nullable=True) health_ratio_level = Column(Float, nullable=False) protocol_id = Column(ChoiceType(ProtocolIDs, impl=String()), nullable=False) diff --git a/apps/web_app/migrations/versions/b3179b2fff8b_ip_address_nullable_true.py b/apps/web_app/migrations/versions/b3179b2fff8b_ip_address_nullable_true.py new file mode 100644 index 00000000..4f1723a8 --- /dev/null +++ b/apps/web_app/migrations/versions/b3179b2fff8b_ip_address_nullable_true.py @@ -0,0 +1,42 @@ +"""ip address nullable true + +Revision ID: b3179b2fff8b +Revises: f4baaac5103f +Create Date: 2024-11-29 18:44:44.613470 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +# revision identifiers, used by Alembic. +revision: str = "b3179b2fff8b" +down_revision: Union[str, None] = "f4baaac5103f" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "notification", + "ip_address", + existing_type=sa.VARCHAR(length=50), + nullable=True, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "notification", + "ip_address", + existing_type=sa.VARCHAR(length=50), + nullable=False, + ) + # ### end Alembic commands ### diff --git a/apps/web_app/telegram/__main__.py b/apps/web_app/telegram/__main__.py index dec7b064..a7b0bfd2 100644 --- a/apps/web_app/telegram/__main__.py +++ b/apps/web_app/telegram/__main__.py @@ -25,6 +25,6 @@ async def bot_start_polling(): if __name__ == "__main__": - if os.getenv("ENV") == "DEV": + if bot is not None: loop = asyncio.get_event_loop() loop.run_until_complete(bot_start_polling()) diff --git a/apps/web_app/telegram/crud.py b/apps/web_app/telegram/crud.py index da4f529f..df78f3d8 100644 --- a/apps/web_app/telegram/crud.py +++ b/apps/web_app/telegram/crud.py @@ -108,3 +108,8 @@ async def get_objects_by_filter( if limit == 1: return await db.scalar(stmp) return await db.scalars(stmp).all() + + async def write_to_db(self, obj: ModelType) -> None: + async with self.Session() as db: + db.add(obj) + await db.commit() diff --git a/apps/web_app/telegram/handlers/create_notification.py b/apps/web_app/telegram/handlers/create_notification.py index babf16ba..c8c2c151 100644 --- a/apps/web_app/telegram/handlers/create_notification.py +++ b/apps/web_app/telegram/handlers/create_notification.py @@ -2,104 +2,131 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from database.models import NotificationData -from database.schemas import NotificationForm from telegram.crud import TelegramCrud from utils.values import ProtocolIDs from fastapi import status create_notification_router = Router() + class NotificationFormStates(StatesGroup): + """States for the notification form process.""" wallet_id = State() health_ratio_level = State() protocol_id = State() + @create_notification_router.callback_query(F.data == "create_subscription") async def start_form(callback: types.CallbackQuery, state: FSMContext): + """Initiates the notification creation form by asking for the wallet ID.""" await state.set_state(NotificationFormStates.wallet_id) await callback.message.edit_text( "Please enter your wallet ID:", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ]) + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ] + ), ) + @create_notification_router.message(NotificationFormStates.wallet_id) async def process_wallet_id(message: types.Message, state: FSMContext): + """Processes the wallet ID input from the user.""" await state.update_data(wallet_id=message.text) await state.set_state(NotificationFormStates.health_ratio_level) await message.answer( "Please enter your health ratio level (between 0 and 10):", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ]) + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ] + ), ) + @create_notification_router.message(NotificationFormStates.health_ratio_level) async def process_health_ratio(message: types.Message, state: FSMContext): + """Processes the health ratio level input from the user.""" try: health_ratio = float(message.text) if not (0 <= health_ratio <= 10): raise ValueError - + await state.update_data(health_ratio_level=health_ratio) await state.set_state(NotificationFormStates.protocol_id) - + # Create protocol selection buttons protocol_buttons = [] for protocol in ProtocolIDs: - protocol_buttons.append([ - types.InlineKeyboardButton( - text=protocol.value, - callback_data=f"protocol_{protocol.value}" - ) - ]) - protocol_buttons.append([ - types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form") - ]) - + protocol_buttons.append( + [ + types.InlineKeyboardButton( + text=protocol.value, callback_data=f"protocol_{protocol.value}" + ) + ] + ) + protocol_buttons.append( + [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] + ) + await message.answer( "Please select your protocol:", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=protocol_buttons) + reply_markup=types.InlineKeyboardMarkup(inline_keyboard=protocol_buttons), ) except ValueError: await message.answer( "Please enter a valid number between 0 and 10.", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ]) + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="Cancel", callback_data="cancel_form" + ) + ] + ] + ), ) + @create_notification_router.callback_query(F.data.startswith("protocol_")) -async def process_protocol(callback: types.CallbackQuery, state: FSMContext, crud: TelegramCrud): +async def process_protocol( + callback: types.CallbackQuery, state: FSMContext, crud: TelegramCrud +): + """Processes the selected protocol and saves the subscription data.""" protocol_id = callback.data.replace("protocol_", "") - form_data = await state.get_data() - - notification = NotificationForm( - wallet_id=form_data["wallet_id"], - health_ratio_level=form_data["health_ratio_level"], + data = await state.get_data() + + subscription = NotificationData( + wallet_id=data["wallet_id"], + health_ratio_level=data["health_ratio_level"], protocol_id=protocol_id, telegram_id=str(callback.from_user.id), - email="" + email=None, + ip_address=None, ) - - subscription = NotificationData(**notification.model_dump()) - subscription_id = crud.write_to_db(obj=subscription) - + await crud.write_to_db(subscription) + await state.clear() await callback.message.edit_text( "Subscription created successfully!", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] - ]) + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] + ] + ), ) + @create_notification_router.callback_query(F.data == "cancel_form") async def cancel_form(callback: types.CallbackQuery, state: FSMContext): + """Cancels the form and clears the state.""" await state.clear() await callback.message.edit_text( "Form cancelled.", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] - ]) - ) \ No newline at end of file + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] + ] + ), + ) From e1fd317ba658a6f7189d486f2a4d38c24a7db176 Mon Sep 17 00:00:00 2001 From: Teri-anric <2005ahi2005@gmail.com> Date: Sat, 30 Nov 2024 21:37:42 +0000 Subject: [PATCH 3/3] improve code structure --- apps/web_app/telegram/crud.py | 6 ++ .../telegram/handlers/create_notification.py | 85 ++++++------------- apps/web_app/telegram/handlers/utils/kb.py | 19 +++++ 3 files changed, 51 insertions(+), 59 deletions(-) diff --git a/apps/web_app/telegram/crud.py b/apps/web_app/telegram/crud.py index df78f3d8..76eeb795 100644 --- a/apps/web_app/telegram/crud.py +++ b/apps/web_app/telegram/crud.py @@ -110,6 +110,12 @@ async def get_objects_by_filter( return await db.scalars(stmp).all() async def write_to_db(self, obj: ModelType) -> None: + """ + Write an object to the database. + + Args: + obj (ModelType): The object to be added to the database. + """ async with self.Session() as db: db.add(obj) await db.commit() diff --git a/apps/web_app/telegram/handlers/create_notification.py b/apps/web_app/telegram/handlers/create_notification.py index c8c2c151..b2404a81 100644 --- a/apps/web_app/telegram/handlers/create_notification.py +++ b/apps/web_app/telegram/handlers/create_notification.py @@ -3,30 +3,31 @@ from aiogram.fsm.state import State, StatesGroup from database.models import NotificationData from telegram.crud import TelegramCrud -from utils.values import ProtocolIDs -from fastapi import status +from .utils import kb create_notification_router = Router() class NotificationFormStates(StatesGroup): """States for the notification form process.""" + wallet_id = State() health_ratio_level = State() protocol_id = State() +# Define constants for health ratio limits +HEALTH_RATIO_MIN = 0 +HEALTH_RATIO_MAX = 10 + + @create_notification_router.callback_query(F.data == "create_subscription") async def start_form(callback: types.CallbackQuery, state: FSMContext): """Initiates the notification creation form by asking for the wallet ID.""" await state.set_state(NotificationFormStates.wallet_id) - await callback.message.edit_text( + return callback.message.edit_text( "Please enter your wallet ID:", - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ] - ), + reply_markup=kb.cancel_form(), ) @@ -35,13 +36,9 @@ async def process_wallet_id(message: types.Message, state: FSMContext): """Processes the wallet ID input from the user.""" await state.update_data(wallet_id=message.text) await state.set_state(NotificationFormStates.health_ratio_level) - await message.answer( + return message.answer( "Please enter your health ratio level (between 0 and 10):", - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ] - ), + reply_markup=kb.cancel_form(), ) @@ -50,44 +47,22 @@ async def process_health_ratio(message: types.Message, state: FSMContext): """Processes the health ratio level input from the user.""" try: health_ratio = float(message.text) - if not (0 <= health_ratio <= 10): + if not (HEALTH_RATIO_MIN <= health_ratio <= HEALTH_RATIO_MAX): raise ValueError - - await state.update_data(health_ratio_level=health_ratio) - await state.set_state(NotificationFormStates.protocol_id) - - # Create protocol selection buttons - protocol_buttons = [] - for protocol in ProtocolIDs: - protocol_buttons.append( - [ - types.InlineKeyboardButton( - text=protocol.value, callback_data=f"protocol_{protocol.value}" - ) - ] - ) - protocol_buttons.append( - [types.InlineKeyboardButton(text="Cancel", callback_data="cancel_form")] - ) - - await message.answer( - "Please select your protocol:", - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=protocol_buttons), - ) except ValueError: - await message.answer( + return message.answer( "Please enter a valid number between 0 and 10.", - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="Cancel", callback_data="cancel_form" - ) - ] - ] - ), + reply_markup=kb.cancel_form(), ) + await state.update_data(health_ratio_level=health_ratio) + await state.set_state(NotificationFormStates.protocol_id) + + return message.answer( + "Please select your protocol:", + reply_markup=kb.protocols(), + ) + @create_notification_router.callback_query(F.data.startswith("protocol_")) async def process_protocol( @@ -108,13 +83,9 @@ async def process_protocol( await crud.write_to_db(subscription) await state.clear() - await callback.message.edit_text( + return callback.message.edit_text( "Subscription created successfully!", - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] - ] - ), + reply_markup=kb.go_menu(), ) @@ -122,11 +93,7 @@ async def process_protocol( async def cancel_form(callback: types.CallbackQuery, state: FSMContext): """Cancels the form and clears the state.""" await state.clear() - await callback.message.edit_text( + return callback.message.edit_text( "Form cancelled.", - reply_markup=types.InlineKeyboardMarkup( - inline_keyboard=[ - [types.InlineKeyboardButton(text="Go to menu", callback_data="go_menu")] - ] - ), + reply_markup=kb.go_menu(), ) diff --git a/apps/web_app/telegram/handlers/utils/kb.py b/apps/web_app/telegram/handlers/utils/kb.py index 610dd6df..b4244c2e 100644 --- a/apps/web_app/telegram/handlers/utils/kb.py +++ b/apps/web_app/telegram/handlers/utils/kb.py @@ -2,6 +2,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder +from utils.values import ProtocolIDs def go_menu(): @@ -103,3 +104,21 @@ def pagination_notifications(curent_uuid: UUID, page: int): if page == 0: markup.adjust(2, 1, 1) return markup.as_markup() + +def cancel_form(): + """ + Returns an InlineKeyboardMarkup with a single button labeled "Cancel" with the callback data "cancel_form". + """ + return InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Cancel", callback_data="cancel_form")]]) + + +def protocols(): + """ + Returns an InlineKeyboardMarkup with buttons for each protocol. + """ + # Create protocol selection buttons + markup = InlineKeyboardBuilder() + for protocol in ProtocolIDs: + markup.button(text=protocol.name, callback_data=f"protocol_{protocol.value}") + markup.button(text="Cancel", callback_data="cancel_form") + return markup.as_markup()