first commit

This commit is contained in:
2026-04-04 21:30:35 +07:00
commit d66041fa31
24 changed files with 503 additions and 0 deletions

1
app/bot/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Aiogram bot wiring (routers, middlewares)."""

View File

@@ -0,0 +1,11 @@
from aiogram import Router
from app.bot.handlers import referral as referral_handlers
from app.bot.handlers import start as start_handlers
def build_root_router() -> Router:
root = Router()
root.include_router(start_handlers.router)
root.include_router(referral_handlers.router)
return root

View File

@@ -0,0 +1,13 @@
"""
Referral UX (invite links, balances, leaderboards) will register here.
Deep-link attribution on /start is handled by ReferralService + UserService.ensure_user.
"""
from aiogram import Router
router = Router(name="referral")
# Example (future):
# @router.message(Command("refer"))
# async def cmd_refer(...): ...

37
app/bot/handlers/start.py Normal file
View File

@@ -0,0 +1,37 @@
from aiogram import Router
from aiogram.filters import CommandObject, CommandStart
from aiogram.types import Message
from app.services.user_service import UserService
router = Router(name="start")
@router.message(CommandStart())
async def cmd_start(
message: Message,
command: CommandObject,
user_service: UserService,
) -> None:
if message.from_user is None:
return
await user_service.ensure_user(
telegram_id=message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
start_payload=command.args,
)
lines = [
'Welcome to <b>MaleniaVPN</b> — <a href="https://malenia.space">malenia.space</a>',
"",
"Your account is linked. VPN controls and billing will appear here as we ship them.",
]
if command.args:
lines.append("")
lines.append(
"<i>Referral link detected; attribution is stored for when rewards go live.</i>"
)
await message.answer("\n".join(lines), parse_mode="HTML")

View File

@@ -0,0 +1,4 @@
from app.bot.middlewares.database import DbSessionMiddleware
from app.bot.middlewares.services import ServicesMiddleware
__all__ = ("DbSessionMiddleware", "ServicesMiddleware")

View File

@@ -0,0 +1,28 @@
from typing import Any, Awaitable, Callable
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
class DbSessionMiddleware(BaseMiddleware):
"""Opens one SQLAlchemy async session per update; commits on success."""
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
self._session_factory = session_factory
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
async with self._session_factory() as session:
data["session"] = session
try:
result = await handler(event, data)
await session.commit()
return result
except Exception:
await session.rollback()
raise

View File

@@ -0,0 +1,25 @@
from typing import Any, Awaitable, Callable
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.user import UserRepository
from app.services.referral_service import ReferralService
from app.services.user_service import UserService
class ServicesMiddleware(BaseMiddleware):
"""Builds application services from the request-scoped session (dependency injection)."""
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
session: AsyncSession = data["session"]
users = UserRepository(session)
referrals = ReferralService(users)
data["user_service"] = UserService(users, referrals)
return await handler(event, data)