diff options
| author | Tolmachev Igor <me@igorek.dev> | 2026-03-23 00:21:18 +0300 |
|---|---|---|
| committer | Tolmachev Igor <me@igorek.dev> | 2026-03-23 00:21:18 +0300 |
| commit | ffee163d8206f0fb1315015e4c60248b68d459bc (patch) | |
| tree | e95f4eb2bb330f2223d00c80fa9e123263657a8e | |
| parent | bc7f486aa7b543a934f4cf23dc80a95f44afcb64 (diff) | |
| download | vpn_manager_bot-ffee163d8206f0fb1315015e4c60248b68d459bc.tar.gz vpn_manager_bot-ffee163d8206f0fb1315015e4c60248b68d459bc.zip | |
Add start, help and vpn_link commands
| -rw-r--r-- | handlers/__init__.py | 5 | ||||
| -rw-r--r-- | handlers/admin/__init__.py | 5 | ||||
| -rw-r--r-- | handlers/middleware.py | 56 | ||||
| -rw-r--r-- | handlers/user/__init__.py | 9 | ||||
| -rw-r--r-- | handlers/user/info.py | 77 | ||||
| -rw-r--r-- | handlers/user/vpn_link.py | 22 | ||||
| -rw-r--r-- | main.py | 12 | ||||
| -rw-r--r-- | pyproject.toml | 1 | ||||
| -rw-r--r-- | uv.lock | 6 |
9 files changed, 189 insertions, 4 deletions
diff --git a/handlers/__init__.py b/handlers/__init__.py index 92143c9..19704d6 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | # isort: off | 1 | # isort: off |
| 2 | from aiogram import Router | 2 | from aiogram import Router |
| 3 | from .middleware import InjectSessionMiddleware, UserAccessMiddleware | ||
| 3 | from . import user | 4 | from . import user |
| 4 | from . import admin | 5 | from . import admin |
| 5 | # isort: on | 6 | # isort: on |
| @@ -9,3 +10,7 @@ router.include_routers( | |||
| 9 | user.router, | 10 | user.router, |
| 10 | admin.router, | 11 | admin.router, |
| 11 | ) | 12 | ) |
| 13 | |||
| 14 | for observer in (router.message, router.callback_query): | ||
| 15 | observer.outer_middleware(InjectSessionMiddleware()) | ||
| 16 | observer.outer_middleware(UserAccessMiddleware()) | ||
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 7197879..d0a5587 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py | |||
| @@ -1,3 +1,6 @@ | |||
| 1 | from aiogram import Router | 1 | from aiogram import F, Router |
| 2 | from aiogram.filters import MagicData | ||
| 2 | 3 | ||
| 3 | router = Router(name="admin") | 4 | router = Router(name="admin") |
| 5 | router.message.filter(MagicData(F.user.is_admin())) | ||
| 6 | router.callback_query.filter(MagicData(F.user.is_admin())) | ||
diff --git a/handlers/middleware.py b/handlers/middleware.py new file mode 100644 index 0000000..87a117a --- /dev/null +++ b/handlers/middleware.py | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | from typing import Any, Awaitable, Callable | ||
| 2 | |||
| 3 | from aiogram import BaseMiddleware | ||
| 4 | from aiogram.types import CallbackQuery, Message, TelegramObject | ||
| 5 | from sqlalchemy.ext.asyncio import AsyncSession | ||
| 6 | |||
| 7 | from database import sessions | ||
| 8 | from models import User | ||
| 9 | |||
| 10 | |||
| 11 | class InjectSessionMiddleware(BaseMiddleware): | ||
| 12 | async def __call__[T]( | ||
| 13 | self, | ||
| 14 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], | ||
| 15 | event: TelegramObject, | ||
| 16 | data: dict[str, Any], | ||
| 17 | ) -> Any: | ||
| 18 | async with sessions.begin() as session: | ||
| 19 | data["session"] = session | ||
| 20 | handler_result = await handler(event, data) | ||
| 21 | return handler_result | ||
| 22 | |||
| 23 | |||
| 24 | class UserAccessMiddleware(BaseMiddleware): | ||
| 25 | async def __call__( | ||
| 26 | self, | ||
| 27 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], | ||
| 28 | event: TelegramObject, | ||
| 29 | data: dict[str, Any], | ||
| 30 | ) -> Any: | ||
| 31 | if not isinstance(event, (Message, CallbackQuery)): | ||
| 32 | raise TypeError( | ||
| 33 | f"UserAccessMiddleware doesn't support event with type: {type(event).__name__}", | ||
| 34 | event, | ||
| 35 | ) | ||
| 36 | |||
| 37 | if isinstance(event, Message): | ||
| 38 | user_id = event.chat.id | ||
| 39 | else: | ||
| 40 | user_id = event.from_user.id | ||
| 41 | |||
| 42 | session: AsyncSession = data["session"] | ||
| 43 | user = await session.get(User, user_id) | ||
| 44 | if user is None: | ||
| 45 | error_text = "Вы не добавлены в список пользователей VPN" | ||
| 46 | |||
| 47 | if isinstance(event, Message): | ||
| 48 | await event.answer(error_text) | ||
| 49 | else: | ||
| 50 | await event.answer(error_text) | ||
| 51 | |||
| 52 | return | ||
| 53 | |||
| 54 | data["user"] = user | ||
| 55 | |||
| 56 | return await handler(event, data) | ||
diff --git a/handlers/user/__init__.py b/handlers/user/__init__.py index 575982f..ac4c0a3 100644 --- a/handlers/user/__init__.py +++ b/handlers/user/__init__.py | |||
| @@ -1,3 +1,12 @@ | |||
| 1 | from aiogram import Router | 1 | from aiogram import Router |
| 2 | 2 | ||
| 3 | # isort: off | ||
| 4 | from . import info | ||
| 5 | from . import vpn_link | ||
| 6 | # isort: on | ||
| 7 | |||
| 3 | router = Router(name="user") | 8 | router = Router(name="user") |
| 9 | router.include_routers( | ||
| 10 | info.router, | ||
| 11 | vpn_link.router, | ||
| 12 | ) | ||
diff --git a/handlers/user/info.py b/handlers/user/info.py new file mode 100644 index 0000000..9776e7e --- /dev/null +++ b/handlers/user/info.py | |||
| @@ -0,0 +1,77 @@ | |||
| 1 | from aiogram import Bot, Router | ||
| 2 | from aiogram.filters import Command | ||
| 3 | from aiogram.types import BotCommand, BotCommandScopeChat, Message | ||
| 4 | |||
| 5 | from models import User | ||
| 6 | |||
| 7 | router = Router(name="info") | ||
| 8 | |||
| 9 | COMMANDS = [ | ||
| 10 | BotCommand( | ||
| 11 | command="start", | ||
| 12 | description="Показать стартовое сообщение", | ||
| 13 | ), | ||
| 14 | BotCommand( | ||
| 15 | command="help", | ||
| 16 | description="Показать список доступных команд", | ||
| 17 | ), | ||
| 18 | BotCommand( | ||
| 19 | command="vpn_link", | ||
| 20 | description="Показать ссылку доступа к VPN", | ||
| 21 | ), | ||
| 22 | BotCommand( | ||
| 23 | command="announcements", | ||
| 24 | description="Показать анонсы", | ||
| 25 | ), | ||
| 26 | BotCommand( | ||
| 27 | command="invoices", | ||
| 28 | description="Показать счета на оплату", | ||
| 29 | ), | ||
| 30 | BotCommand( | ||
| 31 | command="payments", | ||
| 32 | description="Показать платежи", | ||
| 33 | ), | ||
| 34 | BotCommand( | ||
| 35 | command="suggest_user", | ||
| 36 | description="Предложить нового пользователя", | ||
| 37 | ), | ||
| 38 | ] | ||
| 39 | |||
| 40 | ADMIN_COMMANDS = COMMANDS + [ | ||
| 41 | BotCommand( | ||
| 42 | command="new_announcement", | ||
| 43 | description="Создать новый анонс", | ||
| 44 | ), | ||
| 45 | BotCommand( | ||
| 46 | command="new_invoice", | ||
| 47 | description="Создать новый счёт на оплату", | ||
| 48 | ), | ||
| 49 | BotCommand( | ||
| 50 | command="add_user", | ||
| 51 | description="Создать нового пользователя", | ||
| 52 | ), | ||
| 53 | BotCommand( | ||
| 54 | command="suggest_list", | ||
| 55 | description="Показать предложения новых пользователей", | ||
| 56 | ), | ||
| 57 | ] | ||
| 58 | |||
| 59 | |||
| 60 | @router.message(Command("start")) | ||
| 61 | async def start_command(msg: Message, bot: Bot, user: User) -> None: | ||
| 62 | await msg.answer( | ||
| 63 | "Добро пожаловать в бота для пользователей VPN.\n" | ||
| 64 | "Посмотреть список доступных команд: /help" | ||
| 65 | ) | ||
| 66 | |||
| 67 | await bot.set_my_commands( | ||
| 68 | ADMIN_COMMANDS if user.is_admin() else COMMANDS, | ||
| 69 | BotCommandScopeChat(chat_id=user.id), | ||
| 70 | ) | ||
| 71 | |||
| 72 | |||
| 73 | @router.message(Command("help")) | ||
| 74 | async def help_command(msg: Message, user: User) -> None: | ||
| 75 | commands = ADMIN_COMMANDS if user.is_admin() else COMMANDS | ||
| 76 | commands_text = "\n".join(f"/{c.command} - {c.description}" for c in commands) | ||
| 77 | await msg.answer(f"Список доступных команд:\n{commands_text}") | ||
diff --git a/handlers/user/vpn_link.py b/handlers/user/vpn_link.py new file mode 100644 index 0000000..88d2963 --- /dev/null +++ b/handlers/user/vpn_link.py | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | from aiogram import Router | ||
| 2 | from aiogram.filters import Command | ||
| 3 | from aiogram.types import Message | ||
| 4 | |||
| 5 | from models import User | ||
| 6 | |||
| 7 | router = Router(name="vpn_link") | ||
| 8 | |||
| 9 | |||
| 10 | @router.message(Command("vpn_link")) | ||
| 11 | async def vpn_link_command(msg: Message, user: User) -> None: | ||
| 12 | await msg.answer( | ||
| 13 | f"Ссылка для настройки VPN на ваших устройствах:\n{user.vpn_link}" | ||
| 14 | "\n\n" | ||
| 15 | "Пожалуйста не делитесь данной ссылкой с друзьями или знакомыми. " | ||
| 16 | "Данную ссылку разрешено предоставить только родным и близким " | ||
| 17 | "(Родители, Бабушка, Брат, Сестра и т.д.)" | ||
| 18 | "\n" | ||
| 19 | "Если ты вдруг хочешь предоставить VPN другу, то воспользуйся командой /suggest_user" | ||
| 20 | "\n\n" | ||
| 21 | "В противном случае я установлю лимит на количество устройств, а тебе это не нужно. " | ||
| 22 | ) | ||
| @@ -1,6 +1,8 @@ | |||
| 1 | import asyncio | 1 | import asyncio |
| 2 | import logging | 2 | import logging |
| 3 | 3 | ||
| 4 | from aiogram.enums import UpdateType | ||
| 5 | |||
| 4 | from handlers import router | 6 | from handlers import router |
| 5 | from shared import bot, dp | 7 | from shared import bot, dp |
| 6 | 8 | ||
| @@ -8,4 +10,12 @@ logging.basicConfig(level=logging.INFO) | |||
| 8 | 10 | ||
| 9 | dp.include_router(router) | 11 | dp.include_router(router) |
| 10 | 12 | ||
| 11 | asyncio.run(dp.start_polling(bot)) | 13 | asyncio.run( |
| 14 | dp.start_polling( | ||
| 15 | bot, | ||
| 16 | allowed_updates=[ | ||
| 17 | UpdateType.MESSAGE, | ||
| 18 | UpdateType.CALLBACK_QUERY, | ||
| 19 | ], | ||
| 20 | ) | ||
| 21 | ) | ||
diff --git a/pyproject.toml b/pyproject.toml index 4d18619..71a565e 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -8,6 +8,7 @@ dependencies = [ | |||
| 8 | "aiohttp-socks>=0.11.0", | 8 | "aiohttp-socks>=0.11.0", |
| 9 | "aiosqlite>=0.22.1", | 9 | "aiosqlite>=0.22.1", |
| 10 | "alembic>=1.18.4", | 10 | "alembic>=1.18.4", |
| 11 | "magic-filter>=1.0.12", | ||
| 11 | "pydantic>=2.12.5", | 12 | "pydantic>=2.12.5", |
| 12 | "pydantic-settings>=2.13.1", | 13 | "pydantic-settings>=2.13.1", |
| 13 | "sqlalchemy[asyncio]>=2.0.48", | 14 | "sqlalchemy[asyncio]>=2.0.48", |
| @@ -512,14 +512,15 @@ wheels = [ | |||
| 512 | ] | 512 | ] |
| 513 | 513 | ||
| 514 | [[package]] | 514 | [[package]] |
| 515 | name = "vpn-bot" | 515 | name = "vpn-manager-bot" |
| 516 | version = "0.1.0" | 516 | version = "0.0.1" |
| 517 | source = { virtual = "." } | 517 | source = { virtual = "." } |
| 518 | dependencies = [ | 518 | dependencies = [ |
| 519 | { name = "aiogram" }, | 519 | { name = "aiogram" }, |
| 520 | { name = "aiohttp-socks" }, | 520 | { name = "aiohttp-socks" }, |
| 521 | { name = "aiosqlite" }, | 521 | { name = "aiosqlite" }, |
| 522 | { name = "alembic" }, | 522 | { name = "alembic" }, |
| 523 | { name = "magic-filter" }, | ||
| 523 | { name = "pydantic" }, | 524 | { name = "pydantic" }, |
| 524 | { name = "pydantic-settings" }, | 525 | { name = "pydantic-settings" }, |
| 525 | { name = "sqlalchemy", extra = ["asyncio"] }, | 526 | { name = "sqlalchemy", extra = ["asyncio"] }, |
| @@ -531,6 +532,7 @@ requires-dist = [ | |||
| 531 | { name = "aiohttp-socks", specifier = ">=0.11.0" }, | 532 | { name = "aiohttp-socks", specifier = ">=0.11.0" }, |
| 532 | { name = "aiosqlite", specifier = ">=0.22.1" }, | 533 | { name = "aiosqlite", specifier = ">=0.22.1" }, |
| 533 | { name = "alembic", specifier = ">=1.18.4" }, | 534 | { name = "alembic", specifier = ">=1.18.4" }, |
| 535 | { name = "magic-filter", specifier = ">=1.0.12" }, | ||
| 534 | { name = "pydantic", specifier = ">=2.12.5" }, | 536 | { name = "pydantic", specifier = ">=2.12.5" }, |
| 535 | { name = "pydantic-settings", specifier = ">=2.13.1" }, | 537 | { name = "pydantic-settings", specifier = ">=2.13.1" }, |
| 536 | { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.48" }, | 538 | { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.48" }, |
