diff options
| author | Tolmachev Igor <me@igorek.dev> | 2026-04-20 20:56:35 +0300 |
|---|---|---|
| committer | Tolmachev Igor <me@igorek.dev> | 2026-04-20 20:56:35 +0300 |
| commit | 75e99ca0712a2c09230e5c6f8d093dc526cc717d (patch) | |
| tree | f3f00494364a82b866f093651cb9a08030135c4e /handlers | |
| parent | f186fca0f1aa9bbe5eab7613f229df527b2ab774 (diff) | |
| download | vpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.tar.gz vpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.zip | |
Add users command
Diffstat (limited to 'handlers')
| -rw-r--r-- | handlers/admin/__init__.py | 4 | ||||
| -rw-r--r-- | handlers/admin/add_user.py | 99 | ||||
| -rw-r--r-- | handlers/admin/users.py | 557 | ||||
| -rw-r--r-- | handlers/middleware.py | 24 | ||||
| -rw-r--r-- | handlers/user/info.py | 4 | ||||
| -rw-r--r-- | handlers/user/invoices.py | 16 | ||||
| -rw-r--r-- | handlers/user/pay_invoice.py | 5 | ||||
| -rw-r--r-- | handlers/user/payments.py | 12 |
8 files changed, 601 insertions, 120 deletions
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 4593ad7..bc603c0 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py | |||
| @@ -4,7 +4,7 @@ from aiogram.filters import MagicData | |||
| 4 | # isort: off | 4 | # isort: off |
| 5 | from . import new_announcement | 5 | from . import new_announcement |
| 6 | from . import new_invoice | 6 | from . import new_invoice |
| 7 | from . import add_user | 7 | from . import users |
| 8 | from . import payment_status | 8 | from . import payment_status |
| 9 | # isort: on | 9 | # isort: on |
| 10 | 10 | ||
| @@ -15,6 +15,6 @@ router.callback_query.filter(MagicData(F.user.is_admin())) | |||
| 15 | router.include_routers( | 15 | router.include_routers( |
| 16 | new_announcement.router, | 16 | new_announcement.router, |
| 17 | new_invoice.router, | 17 | new_invoice.router, |
| 18 | add_user.router, | 18 | users.router, |
| 19 | payment_status.router, | 19 | payment_status.router, |
| 20 | ) | 20 | ) |
diff --git a/handlers/admin/add_user.py b/handlers/admin/add_user.py deleted file mode 100644 index 1d19834..0000000 --- a/handlers/admin/add_user.py +++ /dev/null | |||
| @@ -1,99 +0,0 @@ | |||
| 1 | from datetime import UTC, datetime | ||
| 2 | |||
| 3 | from aiogram import F, Router | ||
| 4 | from aiogram.enums.button_style import ButtonStyle | ||
| 5 | from aiogram.filters import Command | ||
| 6 | from aiogram.fsm.context import FSMContext | ||
| 7 | from aiogram.fsm.state import State, StatesGroup | ||
| 8 | from aiogram.types import ( | ||
| 9 | KeyboardButton, | ||
| 10 | KeyboardButtonRequestUsers, | ||
| 11 | Message, | ||
| 12 | ReplyKeyboardMarkup, | ||
| 13 | ReplyKeyboardRemove, | ||
| 14 | ) | ||
| 15 | from pydantic import BaseModel | ||
| 16 | from sqlalchemy.ext.asyncio import AsyncSession | ||
| 17 | |||
| 18 | from libs.fsm import edit_data, get_data, set_data | ||
| 19 | from models import User | ||
| 20 | |||
| 21 | router = Router(name="add_user") | ||
| 22 | |||
| 23 | |||
| 24 | class AddUserStates(StatesGroup): | ||
| 25 | user_id = State() | ||
| 26 | vpn_link = State() | ||
| 27 | |||
| 28 | |||
| 29 | class AddUserData(BaseModel): | ||
| 30 | user_id: int | None = None | ||
| 31 | |||
| 32 | |||
| 33 | CANCEL_BUTTON = "Отменить добавление" | ||
| 34 | |||
| 35 | |||
| 36 | @router.message(Command("add_user")) | ||
| 37 | async def command(msg: Message, state: FSMContext) -> None: | ||
| 38 | await msg.answer( | ||
| 39 | "Выберете пользователя которого хотите добавить.", | ||
| 40 | reply_markup=ReplyKeyboardMarkup( | ||
| 41 | keyboard=[ | ||
| 42 | [ | ||
| 43 | KeyboardButton( | ||
| 44 | text="Выбрать пользователя", | ||
| 45 | style=ButtonStyle.PRIMARY, | ||
| 46 | request_users=KeyboardButtonRequestUsers(request_id=0), | ||
| 47 | ), | ||
| 48 | ], | ||
| 49 | [ | ||
| 50 | KeyboardButton(text=CANCEL_BUTTON, style=ButtonStyle.DANGER), | ||
| 51 | ], | ||
| 52 | ], | ||
| 53 | resize_keyboard=True, | ||
| 54 | ), | ||
| 55 | ) | ||
| 56 | await set_data(state, AddUserData(user_id=None)) | ||
| 57 | await state.set_state(AddUserStates.user_id) | ||
| 58 | |||
| 59 | |||
| 60 | @router.message(AddUserStates(), F.text == CANCEL_BUTTON) | ||
| 61 | async def cancel(msg: Message, state: FSMContext) -> None: | ||
| 62 | await msg.answer( | ||
| 63 | "Добавление пользователей отменено", | ||
| 64 | reply_markup=ReplyKeyboardRemove(), | ||
| 65 | ) | ||
| 66 | await state.clear() | ||
| 67 | |||
| 68 | |||
| 69 | @router.message(AddUserStates.user_id) | ||
| 70 | async def set_user_id(msg: Message, state: FSMContext) -> None: | ||
| 71 | if msg.users_shared is None: | ||
| 72 | await msg.answer("Вы должны воспользоваться кнопкой ниже.") | ||
| 73 | return | ||
| 74 | |||
| 75 | async with edit_data(state, AddUserData) as data: | ||
| 76 | data.user_id = msg.users_shared.users[0].user_id | ||
| 77 | |||
| 78 | await msg.answer("Укажите ссылку для доступа к VPN") | ||
| 79 | await state.set_state(AddUserStates.vpn_link) | ||
| 80 | |||
| 81 | |||
| 82 | @router.message(AddUserStates.vpn_link) | ||
| 83 | async def set_vpn_link( | ||
| 84 | msg: Message, | ||
| 85 | state: FSMContext, | ||
| 86 | session: AsyncSession, | ||
| 87 | ) -> None: | ||
| 88 | if msg.text is None: | ||
| 89 | await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.") | ||
| 90 | return | ||
| 91 | |||
| 92 | data = await get_data(state, AddUserData) | ||
| 93 | assert data.user_id is not None | ||
| 94 | |||
| 95 | session.add(User(id=data.user_id, vpn_link=msg.text, datetime=datetime.now(UTC))) | ||
| 96 | await session.flush() | ||
| 97 | |||
| 98 | await msg.answer("Пользователь добавлен.", reply_markup=ReplyKeyboardRemove()) | ||
| 99 | await state.clear() | ||
diff --git a/handlers/admin/users.py b/handlers/admin/users.py new file mode 100644 index 0000000..88dfd89 --- /dev/null +++ b/handlers/admin/users.py | |||
| @@ -0,0 +1,557 @@ | |||
| 1 | from datetime import UTC, datetime | ||
| 2 | from math import ceil | ||
| 3 | |||
| 4 | from aiogram import Bot, F, Router | ||
| 5 | from aiogram.enums import ButtonStyle | ||
| 6 | from aiogram.filters import Command | ||
| 7 | from aiogram.fsm.context import FSMContext | ||
| 8 | from aiogram.fsm.state import State, StatesGroup | ||
| 9 | from aiogram.types import ( | ||
| 10 | CallbackQuery, | ||
| 11 | InlineKeyboardButton, | ||
| 12 | InlineKeyboardMarkup, | ||
| 13 | KeyboardButton, | ||
| 14 | KeyboardButtonRequestUsers, | ||
| 15 | Message, | ||
| 16 | ReplyKeyboardMarkup, | ||
| 17 | ReplyKeyboardRemove, | ||
| 18 | ) | ||
| 19 | from pydantic import BaseModel | ||
| 20 | from sqlalchemy import delete, select | ||
| 21 | from sqlalchemy.ext.asyncio import AsyncSession | ||
| 22 | from sqlalchemy.sql.functions import count | ||
| 23 | |||
| 24 | from libs.fsm import edit_data, get_data, set_data | ||
| 25 | from libs.user import load_user_cache | ||
| 26 | from models import Payment, User, UserRole | ||
| 27 | from models.callback_data import ( | ||
| 28 | UserAddClb, | ||
| 29 | UserDeleteClb, | ||
| 30 | UserDeleteConfirmClb, | ||
| 31 | UserItemClb, | ||
| 32 | UserPageClb, | ||
| 33 | UserRoleClb, | ||
| 34 | UserRoleSetClb, | ||
| 35 | UserVpnLinkClb, | ||
| 36 | ) | ||
| 37 | |||
| 38 | router = Router(name="users") | ||
| 39 | PAGE_SIZE = 5 | ||
| 40 | ROLE_ICON = { | ||
| 41 | UserRole.ADMIN: "👑", | ||
| 42 | UserRole.REGULAR: "👤", | ||
| 43 | } | ||
| 44 | ROLE_NAME = { | ||
| 45 | UserRole.ADMIN: "Администратор", | ||
| 46 | UserRole.REGULAR: "Пользователь", | ||
| 47 | } | ||
| 48 | |||
| 49 | LIST_TEXT = "Список пользователей:" | ||
| 50 | ADD_CANCEL_BUTTON = "Отменить добавление" | ||
| 51 | EDIT_CANCEL_BUTTON = "Отменить изменение" | ||
| 52 | |||
| 53 | |||
| 54 | class AddUserStates(StatesGroup): | ||
| 55 | user_id = State() | ||
| 56 | vpn_link = State() | ||
| 57 | |||
| 58 | |||
| 59 | class AddUserData(BaseModel): | ||
| 60 | user_id: int | None = None | ||
| 61 | |||
| 62 | |||
| 63 | class EditVpnLinkStates(StatesGroup): | ||
| 64 | vpn_link = State() | ||
| 65 | |||
| 66 | |||
| 67 | class EditVpnLinkData(BaseModel): | ||
| 68 | page: int | ||
| 69 | user_id: int | ||
| 70 | |||
| 71 | |||
| 72 | async def get_list_markup( | ||
| 73 | page: int, | ||
| 74 | bot: Bot, | ||
| 75 | session: AsyncSession, | ||
| 76 | ) -> InlineKeyboardMarkup: | ||
| 77 | total = await session.scalar(select(count()).select_from(User)) | ||
| 78 | assert total is not None | ||
| 79 | total_pages = max(1, ceil(total / PAGE_SIZE)) | ||
| 80 | page = max(0, min(page, total_pages - 1)) | ||
| 81 | |||
| 82 | users = await session.scalars( | ||
| 83 | select(User).offset(PAGE_SIZE * page).limit(PAGE_SIZE).order_by(User.id.desc()) | ||
| 84 | ) | ||
| 85 | |||
| 86 | user_buttons = [] | ||
| 87 | for u in users: | ||
| 88 | user_cache = await load_user_cache(bot, u.id) | ||
| 89 | user_buttons.append( | ||
| 90 | [ | ||
| 91 | InlineKeyboardButton( | ||
| 92 | text=f"{ROLE_ICON[u.role]} {user_cache.full_name}", | ||
| 93 | callback_data=UserItemClb(page=page, user_id=u.id).pack(), | ||
| 94 | ) | ||
| 95 | ] | ||
| 96 | ) | ||
| 97 | |||
| 98 | page_buttons = [] | ||
| 99 | if page > 0: | ||
| 100 | page_buttons.append( | ||
| 101 | InlineKeyboardButton( | ||
| 102 | text="◀️", | ||
| 103 | callback_data=UserPageClb(page=page - 1).pack(), | ||
| 104 | ) | ||
| 105 | ) | ||
| 106 | if page < total_pages - 1: | ||
| 107 | page_buttons.append( | ||
| 108 | InlineKeyboardButton( | ||
| 109 | text="▶️", | ||
| 110 | callback_data=UserPageClb(page=page + 1).pack(), | ||
| 111 | ) | ||
| 112 | ) | ||
| 113 | |||
| 114 | add_row = [ | ||
| 115 | InlineKeyboardButton( | ||
| 116 | text="➕ Добавить пользователя", | ||
| 117 | callback_data=UserAddClb().pack(), | ||
| 118 | ) | ||
| 119 | ] | ||
| 120 | |||
| 121 | return InlineKeyboardMarkup(inline_keyboard=[*user_buttons, page_buttons, add_row]) | ||
| 122 | |||
| 123 | |||
| 124 | def build_item_markup(user_id: int, page: int) -> InlineKeyboardMarkup: | ||
| 125 | return InlineKeyboardMarkup( | ||
| 126 | inline_keyboard=[ | ||
| 127 | [ | ||
| 128 | InlineKeyboardButton( | ||
| 129 | text="Поменять роль", | ||
| 130 | callback_data=UserRoleClb(page=page, user_id=user_id).pack(), | ||
| 131 | ), | ||
| 132 | ], | ||
| 133 | [ | ||
| 134 | InlineKeyboardButton( | ||
| 135 | text="Изменить VPN ссылку", | ||
| 136 | callback_data=UserVpnLinkClb(page=page, user_id=user_id).pack(), | ||
| 137 | ), | ||
| 138 | ], | ||
| 139 | [ | ||
| 140 | InlineKeyboardButton( | ||
| 141 | text="Удалить пользователя", | ||
| 142 | style=ButtonStyle.DANGER, | ||
| 143 | callback_data=UserDeleteClb(page=page, user_id=user_id).pack(), | ||
| 144 | ), | ||
| 145 | ], | ||
| 146 | [ | ||
| 147 | InlineKeyboardButton( | ||
| 148 | text="Назад к списку", | ||
| 149 | callback_data=UserPageClb(page=page).pack(), | ||
| 150 | ), | ||
| 151 | ], | ||
| 152 | ] | ||
| 153 | ) | ||
| 154 | |||
| 155 | |||
| 156 | def build_role_markup(user_id: int, page: int) -> InlineKeyboardMarkup: | ||
| 157 | return InlineKeyboardMarkup( | ||
| 158 | inline_keyboard=[ | ||
| 159 | [ | ||
| 160 | InlineKeyboardButton( | ||
| 161 | text=f"{ROLE_ICON[UserRole.REGULAR]} {ROLE_NAME[UserRole.REGULAR]}", | ||
| 162 | callback_data=UserRoleSetClb( | ||
| 163 | page=page, | ||
| 164 | user_id=user_id, | ||
| 165 | role=UserRole.REGULAR, | ||
| 166 | ).pack(), | ||
| 167 | ), | ||
| 168 | ], | ||
| 169 | [ | ||
| 170 | InlineKeyboardButton( | ||
| 171 | text=f"{ROLE_ICON[UserRole.ADMIN]} {ROLE_NAME[UserRole.ADMIN]}", | ||
| 172 | callback_data=UserRoleSetClb( | ||
| 173 | page=page, | ||
| 174 | user_id=user_id, | ||
| 175 | role=UserRole.ADMIN, | ||
| 176 | ).pack(), | ||
| 177 | ), | ||
| 178 | ], | ||
| 179 | [ | ||
| 180 | InlineKeyboardButton( | ||
| 181 | text="Назад", | ||
| 182 | callback_data=UserItemClb(page=page, user_id=user_id).pack(), | ||
| 183 | ), | ||
| 184 | ], | ||
| 185 | ] | ||
| 186 | ) | ||
| 187 | |||
| 188 | |||
| 189 | def build_delete_markup(user_id: int, page: int) -> InlineKeyboardMarkup: | ||
| 190 | return InlineKeyboardMarkup( | ||
| 191 | inline_keyboard=[ | ||
| 192 | [ | ||
| 193 | InlineKeyboardButton( | ||
| 194 | text="❌ Удалить", | ||
| 195 | style=ButtonStyle.DANGER, | ||
| 196 | callback_data=UserDeleteConfirmClb( | ||
| 197 | page=page, | ||
| 198 | user_id=user_id, | ||
| 199 | ).pack(), | ||
| 200 | ), | ||
| 201 | ], | ||
| 202 | [ | ||
| 203 | InlineKeyboardButton( | ||
| 204 | text="Назад", | ||
| 205 | callback_data=UserItemClb(page=page, user_id=user_id).pack(), | ||
| 206 | ), | ||
| 207 | ], | ||
| 208 | ] | ||
| 209 | ) | ||
| 210 | |||
| 211 | |||
| 212 | async def get_item_text(bot: Bot, target: User) -> str: | ||
| 213 | user_cache = await load_user_cache(bot, target.id) | ||
| 214 | return ( | ||
| 215 | f"Пользователь: {user_cache.mention}\n" | ||
| 216 | f"Роль: {ROLE_NAME[target.role]}\n" | ||
| 217 | f"VPN ссылка: {target.vpn_link}" | ||
| 218 | ) | ||
| 219 | |||
| 220 | |||
| 221 | @router.message(Command("users")) | ||
| 222 | async def command(msg: Message, bot: Bot, session: AsyncSession) -> None: | ||
| 223 | await msg.answer( | ||
| 224 | LIST_TEXT, | ||
| 225 | reply_markup=await get_list_markup(0, bot, session), | ||
| 226 | ) | ||
| 227 | |||
| 228 | |||
| 229 | @router.callback_query(UserPageClb.filter()) | ||
| 230 | async def page( | ||
| 231 | clb: CallbackQuery, | ||
| 232 | bot: Bot, | ||
| 233 | callback_data: UserPageClb, | ||
| 234 | session: AsyncSession, | ||
| 235 | ) -> None: | ||
| 236 | assert isinstance(clb.message, Message) | ||
| 237 | await clb.message.edit_text( | ||
| 238 | LIST_TEXT, | ||
| 239 | reply_markup=await get_list_markup(callback_data.page, bot, session), | ||
| 240 | ) | ||
| 241 | await clb.answer() | ||
| 242 | |||
| 243 | |||
| 244 | @router.callback_query(UserItemClb.filter()) | ||
| 245 | async def item( | ||
| 246 | clb: CallbackQuery, | ||
| 247 | bot: Bot, | ||
| 248 | callback_data: UserItemClb, | ||
| 249 | session: AsyncSession, | ||
| 250 | ) -> None: | ||
| 251 | assert isinstance(clb.message, Message) | ||
| 252 | target = await session.get(User, callback_data.user_id) | ||
| 253 | if target is None: | ||
| 254 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 255 | return | ||
| 256 | |||
| 257 | await clb.message.edit_text( | ||
| 258 | await get_item_text(bot, target), | ||
| 259 | reply_markup=build_item_markup(target.id, callback_data.page), | ||
| 260 | ) | ||
| 261 | await clb.answer() | ||
| 262 | |||
| 263 | |||
| 264 | @router.callback_query(UserRoleClb.filter()) | ||
| 265 | async def role_menu( | ||
| 266 | clb: CallbackQuery, | ||
| 267 | callback_data: UserRoleClb, | ||
| 268 | session: AsyncSession, | ||
| 269 | user: User, | ||
| 270 | ) -> None: | ||
| 271 | assert isinstance(clb.message, Message) | ||
| 272 | if user.id == callback_data.user_id: | ||
| 273 | await clb.answer("Нельзя поменять свою роль.", show_alert=True) | ||
| 274 | return | ||
| 275 | |||
| 276 | target = await session.get(User, callback_data.user_id) | ||
| 277 | if target is None: | ||
| 278 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 279 | return | ||
| 280 | |||
| 281 | await clb.message.edit_text( | ||
| 282 | f"Выберете новую роль для пользователя (текущая: {ROLE_NAME[target.role]}):", | ||
| 283 | reply_markup=build_role_markup(target.id, callback_data.page), | ||
| 284 | ) | ||
| 285 | await clb.answer() | ||
| 286 | |||
| 287 | |||
| 288 | @router.callback_query(UserRoleSetClb.filter()) | ||
| 289 | async def role_set( | ||
| 290 | clb: CallbackQuery, | ||
| 291 | bot: Bot, | ||
| 292 | callback_data: UserRoleSetClb, | ||
| 293 | session: AsyncSession, | ||
| 294 | user: User, | ||
| 295 | ) -> None: | ||
| 296 | assert isinstance(clb.message, Message) | ||
| 297 | if user.id == callback_data.user_id: | ||
| 298 | await clb.answer("Нельзя поменять свою роль.", show_alert=True) | ||
| 299 | return | ||
| 300 | |||
| 301 | target = await session.get(User, callback_data.user_id) | ||
| 302 | if target is None: | ||
| 303 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 304 | return | ||
| 305 | |||
| 306 | if target.role != callback_data.role: | ||
| 307 | target.role = callback_data.role | ||
| 308 | await session.flush() | ||
| 309 | |||
| 310 | await clb.message.edit_text( | ||
| 311 | await get_item_text(bot, target), | ||
| 312 | reply_markup=build_item_markup(target.id, callback_data.page), | ||
| 313 | ) | ||
| 314 | await clb.answer(f"Роль: {ROLE_NAME[target.role]}.") | ||
| 315 | |||
| 316 | |||
| 317 | @router.callback_query(UserDeleteClb.filter()) | ||
| 318 | async def delete_menu( | ||
| 319 | clb: CallbackQuery, | ||
| 320 | bot: Bot, | ||
| 321 | callback_data: UserDeleteClb, | ||
| 322 | session: AsyncSession, | ||
| 323 | user: User, | ||
| 324 | ) -> None: | ||
| 325 | assert isinstance(clb.message, Message) | ||
| 326 | if user.id == callback_data.user_id: | ||
| 327 | await clb.answer("Нельзя удалить себя.", show_alert=True) | ||
| 328 | return | ||
| 329 | |||
| 330 | target = await session.get(User, callback_data.user_id) | ||
| 331 | if target is None: | ||
| 332 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 333 | return | ||
| 334 | |||
| 335 | user_cache = await load_user_cache(bot, target.id) | ||
| 336 | await clb.message.edit_text( | ||
| 337 | f"Удалить пользователя {user_cache.mention}?", | ||
| 338 | reply_markup=build_delete_markup(target.id, callback_data.page), | ||
| 339 | ) | ||
| 340 | await clb.answer() | ||
| 341 | |||
| 342 | |||
| 343 | @router.callback_query(UserDeleteConfirmClb.filter()) | ||
| 344 | async def delete_confirm( | ||
| 345 | clb: CallbackQuery, | ||
| 346 | bot: Bot, | ||
| 347 | callback_data: UserDeleteConfirmClb, | ||
| 348 | session: AsyncSession, | ||
| 349 | user: User, | ||
| 350 | ) -> None: | ||
| 351 | assert isinstance(clb.message, Message) | ||
| 352 | if user.id == callback_data.user_id: | ||
| 353 | await clb.answer("Нельзя удалить себя.", show_alert=True) | ||
| 354 | return | ||
| 355 | |||
| 356 | target = await session.get(User, callback_data.user_id) | ||
| 357 | if target is None: | ||
| 358 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 359 | return | ||
| 360 | |||
| 361 | await session.execute(delete(Payment).where(Payment.user_id == target.id)) | ||
| 362 | await session.delete(target) | ||
| 363 | await session.flush() | ||
| 364 | |||
| 365 | await clb.message.edit_text( | ||
| 366 | LIST_TEXT, | ||
| 367 | reply_markup=await get_list_markup(callback_data.page, bot, session), | ||
| 368 | ) | ||
| 369 | await clb.answer("Пользователь удалён.") | ||
| 370 | |||
| 371 | |||
| 372 | @router.callback_query(UserVpnLinkClb.filter()) | ||
| 373 | async def vpn_link_button( | ||
| 374 | clb: CallbackQuery, | ||
| 375 | bot: Bot, | ||
| 376 | state: FSMContext, | ||
| 377 | callback_data: UserVpnLinkClb, | ||
| 378 | session: AsyncSession, | ||
| 379 | ) -> None: | ||
| 380 | assert isinstance(clb.message, Message) | ||
| 381 | target = await session.get(User, callback_data.user_id) | ||
| 382 | if target is None: | ||
| 383 | await clb.answer("Пользователь не найден.", show_alert=True) | ||
| 384 | return | ||
| 385 | |||
| 386 | await clb.message.delete() | ||
| 387 | await bot.send_message( | ||
| 388 | clb.from_user.id, | ||
| 389 | "Укажите новую ссылку для доступа к VPN.", | ||
| 390 | reply_markup=ReplyKeyboardMarkup( | ||
| 391 | keyboard=[ | ||
| 392 | [KeyboardButton(text=EDIT_CANCEL_BUTTON, style=ButtonStyle.DANGER)] | ||
| 393 | ], | ||
| 394 | resize_keyboard=True, | ||
| 395 | ), | ||
| 396 | ) | ||
| 397 | await set_data( | ||
| 398 | state, | ||
| 399 | EditVpnLinkData(page=callback_data.page, user_id=callback_data.user_id), | ||
| 400 | ) | ||
| 401 | await state.set_state(EditVpnLinkStates.vpn_link) | ||
| 402 | await clb.answer() | ||
| 403 | |||
| 404 | |||
| 405 | async def send_item(bot: Bot, chat_id: int, target: User, page: int) -> None: | ||
| 406 | await bot.send_message( | ||
| 407 | chat_id, | ||
| 408 | await get_item_text(bot, target), | ||
| 409 | reply_markup=build_item_markup(target.id, page), | ||
| 410 | ) | ||
| 411 | |||
| 412 | |||
| 413 | @router.message(EditVpnLinkStates.vpn_link, F.text == EDIT_CANCEL_BUTTON) | ||
| 414 | async def vpn_link_cancel( | ||
| 415 | msg: Message, | ||
| 416 | bot: Bot, | ||
| 417 | state: FSMContext, | ||
| 418 | session: AsyncSession, | ||
| 419 | ) -> None: | ||
| 420 | data = await get_data(state, EditVpnLinkData) | ||
| 421 | await state.clear() | ||
| 422 | |||
| 423 | await msg.answer( | ||
| 424 | "Изменение VPN ссылки отменено.", | ||
| 425 | reply_markup=ReplyKeyboardRemove(), | ||
| 426 | ) | ||
| 427 | |||
| 428 | target = await session.get(User, data.user_id) | ||
| 429 | if target is None: | ||
| 430 | return | ||
| 431 | await send_item(bot, msg.chat.id, target, data.page) | ||
| 432 | |||
| 433 | |||
| 434 | @router.message(EditVpnLinkStates.vpn_link) | ||
| 435 | async def vpn_link_set( | ||
| 436 | msg: Message, | ||
| 437 | bot: Bot, | ||
| 438 | state: FSMContext, | ||
| 439 | session: AsyncSession, | ||
| 440 | ) -> None: | ||
| 441 | if msg.text is None: | ||
| 442 | await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.") | ||
| 443 | return | ||
| 444 | |||
| 445 | data = await get_data(state, EditVpnLinkData) | ||
| 446 | target = await session.get(User, data.user_id) | ||
| 447 | if target is None: | ||
| 448 | await msg.answer( | ||
| 449 | "Пользователь не найден.", | ||
| 450 | reply_markup=ReplyKeyboardRemove(), | ||
| 451 | ) | ||
| 452 | await state.clear() | ||
| 453 | return | ||
| 454 | |||
| 455 | target.vpn_link = msg.text | ||
| 456 | await session.flush() | ||
| 457 | await state.clear() | ||
| 458 | |||
| 459 | await msg.answer("VPN ссылка обновлена.", reply_markup=ReplyKeyboardRemove()) | ||
| 460 | await send_item(bot, msg.chat.id, target, data.page) | ||
| 461 | |||
| 462 | |||
| 463 | @router.callback_query(UserAddClb.filter()) | ||
| 464 | async def add_button( | ||
| 465 | clb: CallbackQuery, | ||
| 466 | bot: Bot, | ||
| 467 | state: FSMContext, | ||
| 468 | ) -> None: | ||
| 469 | assert isinstance(clb.message, Message) | ||
| 470 | await clb.message.delete() | ||
| 471 | await bot.send_message( | ||
| 472 | clb.from_user.id, | ||
| 473 | "Выберете пользователя которого хотите добавить.", | ||
| 474 | reply_markup=ReplyKeyboardMarkup( | ||
| 475 | keyboard=[ | ||
| 476 | [ | ||
| 477 | KeyboardButton( | ||
| 478 | text="Выбрать пользователя", | ||
| 479 | style=ButtonStyle.PRIMARY, | ||
| 480 | request_users=KeyboardButtonRequestUsers(request_id=0), | ||
| 481 | ), | ||
| 482 | ], | ||
| 483 | [KeyboardButton(text=ADD_CANCEL_BUTTON, style=ButtonStyle.DANGER)], | ||
| 484 | ], | ||
| 485 | resize_keyboard=True, | ||
| 486 | ), | ||
| 487 | ) | ||
| 488 | await set_data(state, AddUserData(user_id=None)) | ||
| 489 | await state.set_state(AddUserStates.user_id) | ||
| 490 | await clb.answer() | ||
| 491 | |||
| 492 | |||
| 493 | @router.message(AddUserStates(), F.text == ADD_CANCEL_BUTTON) | ||
| 494 | async def add_cancel( | ||
| 495 | msg: Message, | ||
| 496 | bot: Bot, | ||
| 497 | state: FSMContext, | ||
| 498 | session: AsyncSession, | ||
| 499 | ) -> None: | ||
| 500 | await msg.answer( | ||
| 501 | "Добавление пользователя отменено.", | ||
| 502 | reply_markup=ReplyKeyboardRemove(), | ||
| 503 | ) | ||
| 504 | |||
| 505 | await msg.answer( | ||
| 506 | LIST_TEXT, | ||
| 507 | reply_markup=await get_list_markup(0, bot, session), | ||
| 508 | ) | ||
| 509 | await state.clear() | ||
| 510 | |||
| 511 | |||
| 512 | @router.message(AddUserStates.user_id) | ||
| 513 | async def add_set_user_id(msg: Message, state: FSMContext) -> None: | ||
| 514 | if msg.users_shared is None: | ||
| 515 | await msg.answer("Вы должны воспользоваться кнопкой ниже.") | ||
| 516 | return | ||
| 517 | |||
| 518 | async with edit_data(state, AddUserData) as data: | ||
| 519 | data.user_id = msg.users_shared.users[0].user_id | ||
| 520 | |||
| 521 | await msg.answer( | ||
| 522 | "Укажите ссылку для доступа к VPN.", | ||
| 523 | reply_markup=ReplyKeyboardMarkup( | ||
| 524 | keyboard=[ | ||
| 525 | [KeyboardButton(text=ADD_CANCEL_BUTTON, style=ButtonStyle.DANGER)] | ||
| 526 | ], | ||
| 527 | resize_keyboard=True, | ||
| 528 | ), | ||
| 529 | ) | ||
| 530 | await state.set_state(AddUserStates.vpn_link) | ||
| 531 | |||
| 532 | |||
| 533 | @router.message(AddUserStates.vpn_link) | ||
| 534 | async def add_set_vpn_link( | ||
| 535 | msg: Message, | ||
| 536 | bot: Bot, | ||
| 537 | state: FSMContext, | ||
| 538 | session: AsyncSession, | ||
| 539 | ) -> None: | ||
| 540 | if msg.text is None: | ||
| 541 | await msg.answer("Вы должны указать ссылку отправив текстовое сообщение.") | ||
| 542 | return | ||
| 543 | |||
| 544 | data = await get_data(state, AddUserData) | ||
| 545 | assert data.user_id is not None | ||
| 546 | |||
| 547 | session.add(User(id=data.user_id, vpn_link=msg.text, datetime=datetime.now(UTC))) | ||
| 548 | await session.flush() | ||
| 549 | |||
| 550 | await msg.answer("Пользователь добавлен.", reply_markup=ReplyKeyboardRemove()) | ||
| 551 | |||
| 552 | target = await session.get(User, data.user_id) | ||
| 553 | if target is None: | ||
| 554 | return | ||
| 555 | |||
| 556 | await send_item(bot, msg.chat.id, target, 0) | ||
| 557 | await state.clear() | ||
diff --git a/handlers/middleware.py b/handlers/middleware.py index 87a117a..376f4a2 100644 --- a/handlers/middleware.py +++ b/handlers/middleware.py | |||
| @@ -5,7 +5,9 @@ from aiogram.types import CallbackQuery, Message, TelegramObject | |||
| 5 | from sqlalchemy.ext.asyncio import AsyncSession | 5 | from sqlalchemy.ext.asyncio import AsyncSession |
| 6 | 6 | ||
| 7 | from database import sessions | 7 | from database import sessions |
| 8 | from libs.user import set_user_cache | ||
| 8 | from models import User | 9 | from models import User |
| 10 | from models.user import UserCache | ||
| 9 | 11 | ||
| 10 | 12 | ||
| 11 | class InjectSessionMiddleware(BaseMiddleware): | 13 | class InjectSessionMiddleware(BaseMiddleware): |
| @@ -54,3 +56,25 @@ class UserAccessMiddleware(BaseMiddleware): | |||
| 54 | data["user"] = user | 56 | data["user"] = user |
| 55 | 57 | ||
| 56 | return await handler(event, data) | 58 | return await handler(event, data) |
| 59 | |||
| 60 | |||
| 61 | class UserCacheMiddleware(BaseMiddleware): | ||
| 62 | async def __call__( | ||
| 63 | self, | ||
| 64 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], | ||
| 65 | event: TelegramObject, | ||
| 66 | data: dict[str, Any], | ||
| 67 | ) -> Any: | ||
| 68 | if not isinstance(event, (Message, CallbackQuery)): | ||
| 69 | raise TypeError( | ||
| 70 | f"UserAccessMiddleware doesn't support event with type: {type(event).__name__}", | ||
| 71 | event, | ||
| 72 | ) | ||
| 73 | |||
| 74 | if isinstance(event, Message): | ||
| 75 | user_cache = UserCache.from_chat(event.chat) | ||
| 76 | else: | ||
| 77 | user_cache = UserCache.from_user(event.from_user) | ||
| 78 | |||
| 79 | await set_user_cache(user_cache) | ||
| 80 | return await handler(event, data) | ||
diff --git a/handlers/user/info.py b/handlers/user/info.py index 9776e7e..587925f 100644 --- a/handlers/user/info.py +++ b/handlers/user/info.py | |||
| @@ -47,8 +47,8 @@ ADMIN_COMMANDS = COMMANDS + [ | |||
| 47 | description="Создать новый счёт на оплату", | 47 | description="Создать новый счёт на оплату", |
| 48 | ), | 48 | ), |
| 49 | BotCommand( | 49 | BotCommand( |
| 50 | command="add_user", | 50 | command="users", |
| 51 | description="дть пользователя", | 51 | description="ра пользователями", |
| 52 | ), | 52 | ), |
| 53 | BotCommand( | 53 | BotCommand( |
| 54 | command="suggest_list", | 54 | command="suggest_list", |
diff --git a/handlers/user/invoices.py b/handlers/user/invoices.py index cc071bb..15785fb 100644 --- a/handlers/user/invoices.py +++ b/handlers/user/invoices.py | |||
| @@ -18,7 +18,7 @@ from libs.invoice import ( | |||
| 18 | get_payment_status, | 18 | get_payment_status, |
| 19 | ) | 19 | ) |
| 20 | from libs.msg import eclipse_text | 20 | from libs.msg import eclipse_text |
| 21 | from libs.user import mention | 21 | from libs.user import load_user_cache |
| 22 | from models import Invoice, PaymentStatus, User | 22 | from models import Invoice, PaymentStatus, User |
| 23 | from models.callback_data import InvoiceItemClb, InvoicePageClb, PayInvoiceClb | 23 | from models.callback_data import InvoiceItemClb, InvoicePageClb, PayInvoiceClb |
| 24 | 24 | ||
| @@ -166,14 +166,14 @@ async def item( | |||
| 166 | ] | 166 | ] |
| 167 | ) | 167 | ) |
| 168 | 168 | ||
| 169 | await clb.message.edit_text(text_template.format("..."), reply_markup=reply_markup) | ||
| 170 | user_status = [] | 169 | user_status = [] |
| 171 | for user_id, s in invoice_payments.user_status.items(): | 170 | for user_id, s in invoice_payments.user_status.items(): |
| 172 | chat = await bot.get_chat(user_id) | 171 | user_cache = await load_user_cache(bot, user_id) |
| 173 | user_status.append(f"{PAYMENT_STATUS[s]} - {mention(chat)}") | 172 | user_status.append(f"{PAYMENT_STATUS[s]} - {user_cache.mention}") |
| 174 | await clb.message.edit_text( | 173 | |
| 175 | text_template.format("\n".join(user_status)), | 174 | await clb.message.edit_text( |
| 176 | reply_markup=reply_markup, | 175 | text_template.format("\n".join(user_status)), |
| 177 | ) | 176 | reply_markup=reply_markup, |
| 177 | ) | ||
| 178 | 178 | ||
| 179 | await clb.answer() | 179 | await clb.answer() |
diff --git a/handlers/user/pay_invoice.py b/handlers/user/pay_invoice.py index db75f47..d7809cd 100644 --- a/handlers/user/pay_invoice.py +++ b/handlers/user/pay_invoice.py | |||
| @@ -19,7 +19,6 @@ from sqlalchemy import and_, select | |||
| 19 | from sqlalchemy.ext.asyncio import AsyncSession | 19 | from sqlalchemy.ext.asyncio import AsyncSession |
| 20 | 20 | ||
| 21 | from libs.fsm import get_data, set_data | 21 | from libs.fsm import get_data, set_data |
| 22 | from libs.user import mention | ||
| 23 | from models import ( | 22 | from models import ( |
| 24 | Invoice, | 23 | Invoice, |
| 25 | Payment, | 24 | Payment, |
| @@ -27,6 +26,7 @@ from models import ( | |||
| 27 | ReceiptFile, | 26 | ReceiptFile, |
| 28 | ReceiptFileType, | 27 | ReceiptFileType, |
| 29 | User, | 28 | User, |
| 29 | UserCache, | ||
| 30 | UserRole, | 30 | UserRole, |
| 31 | ) | 31 | ) |
| 32 | from models.callback_data import PayInvoiceClb, PaymentStatusClb | 32 | from models.callback_data import PayInvoiceClb, PaymentStatusClb |
| @@ -112,6 +112,7 @@ async def receipt( | |||
| 112 | bot: Bot, | 112 | bot: Bot, |
| 113 | state: FSMContext, | 113 | state: FSMContext, |
| 114 | session: AsyncSession, | 114 | session: AsyncSession, |
| 115 | user_cache: UserCache, | ||
| 115 | ) -> None: | 116 | ) -> None: |
| 116 | if msg.document is not None: | 117 | if msg.document is not None: |
| 117 | receipt_file = ReceiptFile( | 118 | receipt_file = ReceiptFile( |
| @@ -173,7 +174,7 @@ async def receipt( | |||
| 173 | try: | 174 | try: |
| 174 | await bot.send_message( | 175 | await bot.send_message( |
| 175 | admin_id, | 176 | admin_id, |
| 176 | f"Новое подтверждение оплаты:\nПользователь: {mention(msg.chat)}", | 177 | f"Новое подтверждение оплаты:\nПользователь: {user_cache.mention}", |
| 177 | ) | 178 | ) |
| 178 | await receipt_file.send(bot, admin_id, reply_markup=reply_markup) | 179 | await receipt_file.send(bot, admin_id, reply_markup=reply_markup) |
| 179 | except TelegramAPIError as e: | 180 | except TelegramAPIError as e: |
diff --git a/handlers/user/payments.py b/handlers/user/payments.py index 87ea236..e15a882 100644 --- a/handlers/user/payments.py +++ b/handlers/user/payments.py | |||
| @@ -15,13 +15,13 @@ from sqlalchemy.sql.functions import count | |||
| 15 | 15 | ||
| 16 | from libs.invoice import get_payment_status | 16 | from libs.invoice import get_payment_status |
| 17 | from libs.msg import eclipse_text | 17 | from libs.msg import eclipse_text |
| 18 | from libs.user import mention | 18 | from libs.user import load_user_cache |
| 19 | from models import Invoice, Payment, PaymentStatus, User | 19 | from models import Invoice, Payment, PaymentStatus, User |
| 20 | from models.callback_data import ( | 20 | from models.callback_data import ( |
| 21 | PayInvoiceClb, | ||
| 21 | PaymentItemClb, | 22 | PaymentItemClb, |
| 22 | PaymentPageClb, | 23 | PaymentPageClb, |
| 23 | PaymentStatusClb, | 24 | PaymentStatusClb, |
| 24 | PayInvoiceClb, | ||
| 25 | ) | 25 | ) |
| 26 | 26 | ||
| 27 | router = Router(name="payments") | 27 | router = Router(name="payments") |
| @@ -213,7 +213,7 @@ async def item( | |||
| 213 | 213 | ||
| 214 | invoice = await session.get(Invoice, payment.invoice_id) | 214 | invoice = await session.get(Invoice, payment.invoice_id) |
| 215 | assert invoice is not None | 215 | assert invoice is not None |
| 216 | chat = await bot.get_chat(payment.user_id) | 216 | user_cache = await load_user_cache(bot, payment.user_id) |
| 217 | 217 | ||
| 218 | status_buttons = [] | 218 | status_buttons = [] |
| 219 | if payment.status != PaymentStatus.ACCEPTED: | 219 | if payment.status != PaymentStatus.ACCEPTED: |
| @@ -242,12 +242,10 @@ async def item( | |||
| 242 | text="Назад к выбору", | 242 | text="Назад к выбору", |
| 243 | callback_data=PaymentPageClb(page=callback_data.page).pack(), | 243 | callback_data=PaymentPageClb(page=callback_data.page).pack(), |
| 244 | ) | 244 | ) |
| 245 | reply_markup = InlineKeyboardMarkup( | 245 | reply_markup = InlineKeyboardMarkup(inline_keyboard=[status_buttons, [back_button]]) |
| 246 | inline_keyboard=[status_buttons, [back_button]] | ||
| 247 | ) | ||
| 248 | 246 | ||
| 249 | caption = ( | 247 | caption = ( |
| 250 | f"Платёж от {mention(chat)}\n" | 248 | f"Платёж от {user_cache.mention}\n" |
| 251 | f"Счёт: {eclipse_text(invoice.message.text, 30)}\n" | 249 | f"Счёт: {eclipse_text(invoice.message.text, 30)}\n" |
| 252 | f"Дата: {payment.datetime.strftime('%d %b %y г.')}\n" | 250 | f"Дата: {payment.datetime.strftime('%d %b %y г.')}\n" |
| 253 | f"Статус: {PAYMENT_STATUS[payment.status]}" | 251 | f"Статус: {PAYMENT_STATUS[payment.status]}" |
