From 2a1f15fdf2a50ba7bc209bdb25f9383ac59abe38 Mon Sep 17 00:00:00 2001 From: ruslangilfanov Date: Sat, 16 Aug 2025 18:55:05 +0300 Subject: [PATCH] add user model, commands handler, router setup, and related dependencies --- pyproject.toml | 1 + src/greek_lang/database/app_models.py | 2 + .../versions/20250816_1753_747797032526_.py | 54 +++++++++++++++++++ src/greek_lang/tg_bot/app.py | 19 ++----- src/greek_lang/tg_bot/commands.py | 23 ++++++++ src/greek_lang/tg_bot/router.py | 8 +++ src/greek_lang/users/__init__.py | 0 src/greek_lang/users/manager.py | 39 ++++++++++++++ src/greek_lang/users/models.py | 26 +++++++++ src/greek_lang/utils/telegram_log/handler.py | 2 +- uv.lock | 14 +++++ 11 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 src/greek_lang/database/migrations/versions/20250816_1753_747797032526_.py create mode 100644 src/greek_lang/tg_bot/commands.py create mode 100644 src/greek_lang/tg_bot/router.py create mode 100644 src/greek_lang/users/__init__.py create mode 100644 src/greek_lang/users/manager.py create mode 100644 src/greek_lang/users/models.py diff --git a/pyproject.toml b/pyproject.toml index 227abb4..8fb57f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,4 +46,5 @@ dev = [ "pre-commit>=4.2.0", "pyupgrade>=3.20.0", "ruff>=0.11.13", + "types-requests>=2.32.4.20250809", ] diff --git a/src/greek_lang/database/app_models.py b/src/greek_lang/database/app_models.py index 3505dc4..e8af15a 100644 --- a/src/greek_lang/database/app_models.py +++ b/src/greek_lang/database/app_models.py @@ -4,8 +4,10 @@ import types def get_app_models_modules() -> list[types.ModuleType]: from greek_lang.glossaries import models as glossaries_models from greek_lang.openai_manager import models as openai_manager_models + from greek_lang.users import models as users_models return [ glossaries_models, openai_manager_models, + users_models, ] diff --git a/src/greek_lang/database/migrations/versions/20250816_1753_747797032526_.py b/src/greek_lang/database/migrations/versions/20250816_1753_747797032526_.py new file mode 100644 index 0000000..da6828c --- /dev/null +++ b/src/greek_lang/database/migrations/versions/20250816_1753_747797032526_.py @@ -0,0 +1,54 @@ +"""empty message + +Revision ID: 747797032526 +Revises: d30d80dee5a3 +Create Date: 2025-08-16 17:53:23.785592 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "747797032526" +down_revision: Union[str, None] = "d30d80dee5a3" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "users", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column("is_bot", sa.Boolean(), nullable=False), + sa.Column("first_name", sa.String(), nullable=True), + sa.Column("last_name", sa.String(), nullable=True), + sa.Column("username", sa.String(), nullable=True), + sa.Column("language_code", sa.String(length=8), nullable=True), + sa.Column("is_premium", sa.Boolean(), nullable=True), + sa.Column("added_to_attachment_menu", sa.Boolean(), nullable=True), + sa.Column( + "registered_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id", name=op.f("pk_users")), + ) + op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False) + op.create_index(op.f("ix_users_username"), "users", ["username"], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_users_username"), table_name="users") + op.drop_index(op.f("ix_users_id"), table_name="users") + op.drop_table("users") + # ### end Alembic commands ### diff --git a/src/greek_lang/tg_bot/app.py b/src/greek_lang/tg_bot/app.py index e6093a5..dff7d6f 100644 --- a/src/greek_lang/tg_bot/app.py +++ b/src/greek_lang/tg_bot/app.py @@ -1,11 +1,8 @@ import pydantic from aiogram import Bot, Dispatcher, BaseMiddleware -from aiogram.enums import ParseMode -from aiogram.filters import CommandStart from aiogram.fsm.storage.base import BaseStorage, DefaultKeyBuilder from aiogram.fsm.storage.redis import RedisStorage -from aiogram.types import BotCommandScopeAllPrivateChats, Message -from aiogram_dialog import DialogManager, setup_dialogs +from aiogram.types import BotCommandScopeAllPrivateChats from dependency_injector.wiring import Provide, inject from ..configs.container import ConfigContainer @@ -26,14 +23,7 @@ def create_bot( async def create_dispatcher() -> Dispatcher: - async def start(message: Message, dialog_manager: DialogManager) -> None: - user = message.from_user - if user is None: - return - await message.answer( - text="TEST", - parse_mode=ParseMode.HTML, - ) + from .router import router as root_router fsm_storage = await create_fsm_storage() @@ -45,10 +35,7 @@ async def create_dispatcher() -> Dispatcher: for middleware in middlewares: dp.update.middleware(middleware) - dp.message.register(start, CommandStart()) - dp.business_message.register(start, CommandStart()) - - setup_dialogs(dp) + dp.include_routers(root_router) return dp diff --git a/src/greek_lang/tg_bot/commands.py b/src/greek_lang/tg_bot/commands.py new file mode 100644 index 0000000..7d221fe --- /dev/null +++ b/src/greek_lang/tg_bot/commands.py @@ -0,0 +1,23 @@ +from gettext import gettext + +from aiogram import Router +from aiogram.enums import ParseMode +from aiogram.filters import CommandStart +from aiogram.types import Message + +from greek_lang.users.manager import get_or_create_telegram_user + + +router = Router() + + +@router.message(CommandStart()) +async def start(message: Message) -> None: + user = message.from_user + if user is None: + return + await get_or_create_telegram_user(user) + await message.answer( + text=gettext("Регистрация прошла.").format(user=user.full_name), + parse_mode=ParseMode.HTML, + ) diff --git a/src/greek_lang/tg_bot/router.py b/src/greek_lang/tg_bot/router.py new file mode 100644 index 0000000..33b973e --- /dev/null +++ b/src/greek_lang/tg_bot/router.py @@ -0,0 +1,8 @@ +from aiogram import Router + +from .commands import router as commands_router + +router = Router() +router.include_routers( + commands_router, +) diff --git a/src/greek_lang/users/__init__.py b/src/greek_lang/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/greek_lang/users/manager.py b/src/greek_lang/users/manager.py new file mode 100644 index 0000000..a981ba7 --- /dev/null +++ b/src/greek_lang/users/manager.py @@ -0,0 +1,39 @@ +from aiogram.types import User +from dependency_injector.wiring import Provide, inject +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + +from greek_lang.database.container import DatabaseContainer +from greek_lang.users.models import User as TgUser + + +@inject +async def get_or_create_telegram_user( + user: User, + db_session_maker: async_sessionmaker[AsyncSession] = Provide[ + DatabaseContainer.async_session_maker + ], +) -> TgUser: + async with db_session_maker() as db_session, db_session.begin(): + telegram_user: TgUser | None = await db_session.get(TgUser, user.id) + if telegram_user: + return telegram_user + try: + async with db_session_maker() as db_session, db_session.begin(): + telegram_user = TgUser( + id=user.id, + username=user.username, + first_name=user.first_name, + last_name=user.last_name, + language_code=user.language_code, + is_bot=user.is_bot, + is_premium=user.is_premium, + added_to_attachment_menu=user.added_to_attachment_menu, + ) + db_session.add(telegram_user) + return telegram_user + except IntegrityError: + telegram_user = await db_session.get(TgUser, user.id) + if telegram_user is None: + raise Exception(f"Can't find telegram_user = {user.id}") from None + return telegram_user diff --git a/src/greek_lang/users/models.py b/src/greek_lang/users/models.py new file mode 100644 index 0000000..01e3570 --- /dev/null +++ b/src/greek_lang/users/models.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from sqlalchemy import BigInteger, Boolean, DateTime, String, func +from sqlalchemy.orm import Mapped, mapped_column + +from greek_lang.database.base import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True, index=True) + is_bot: Mapped[bool] = mapped_column(Boolean, nullable=False) + first_name: Mapped[str | None] = mapped_column(String, nullable=True) + last_name: Mapped[str | None] = mapped_column(String, nullable=True) + username: Mapped[str | None] = mapped_column(String, nullable=True, index=True) + language_code: Mapped[str | None] = mapped_column(String(length=8), nullable=True) + is_premium: Mapped[bool | None] = mapped_column(Boolean, nullable=True) + added_to_attachment_menu: Mapped[bool | None] = mapped_column( + Boolean, nullable=True + ) + registered_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ) diff --git a/src/greek_lang/utils/telegram_log/handler.py b/src/greek_lang/utils/telegram_log/handler.py index 94a3cbb..e547dd6 100644 --- a/src/greek_lang/utils/telegram_log/handler.py +++ b/src/greek_lang/utils/telegram_log/handler.py @@ -6,7 +6,7 @@ import logging import logging.config import types -import cgitb +import cgitb # type: ignore[import-untyped] import telebot from requests import ReadTimeout diff --git a/uv.lock b/uv.lock index 2ae29da..6e61916 100644 --- a/uv.lock +++ b/uv.lock @@ -417,6 +417,7 @@ dev = [ { name = "pre-commit" }, { name = "pyupgrade" }, { name = "ruff" }, + { name = "types-requests" }, ] [package.metadata] @@ -452,6 +453,7 @@ dev = [ { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pyupgrade", specifier = ">=3.20.0" }, { name = "ruff", specifier = ">=0.11.13" }, + { name = "types-requests", specifier = ">=2.32.4.20250809" }, ] [[package]] @@ -1292,6 +1294,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20250809" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1"