aboutsummaryrefslogtreecommitdiff
path: root/handlers/admin/users.py
diff options
context:
space:
mode:
authorTolmachev Igor <me@igorek.dev>2026-04-20 20:56:35 +0300
committerTolmachev Igor <me@igorek.dev>2026-04-20 20:56:35 +0300
commit75e99ca0712a2c09230e5c6f8d093dc526cc717d (patch)
treef3f00494364a82b866f093651cb9a08030135c4e /handlers/admin/users.py
parentf186fca0f1aa9bbe5eab7613f229df527b2ab774 (diff)
downloadvpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.tar.gz
vpn_manager_bot-75e99ca0712a2c09230e5c6f8d093dc526cc717d.zip
Add users command
Diffstat (limited to 'handlers/admin/users.py')
-rw-r--r--handlers/admin/users.py557
1 files changed, 557 insertions, 0 deletions
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 @@
1from datetime import UTC, datetime
2from math import ceil
3
4from aiogram import Bot, F, Router
5from aiogram.enums import ButtonStyle
6from aiogram.filters import Command
7from aiogram.fsm.context import FSMContext
8from aiogram.fsm.state import State, StatesGroup
9from aiogram.types import (
10 CallbackQuery,
11 InlineKeyboardButton,
12 InlineKeyboardMarkup,
13 KeyboardButton,
14 KeyboardButtonRequestUsers,
15 Message,
16 ReplyKeyboardMarkup,
17 ReplyKeyboardRemove,
18)
19from pydantic import BaseModel
20from sqlalchemy import delete, select
21from sqlalchemy.ext.asyncio import AsyncSession
22from sqlalchemy.sql.functions import count
23
24from libs.fsm import edit_data, get_data, set_data
25from libs.user import load_user_cache
26from models import Payment, User, UserRole
27from models.callback_data import (
28 UserAddClb,
29 UserDeleteClb,
30 UserDeleteConfirmClb,
31 UserItemClb,
32 UserPageClb,
33 UserRoleClb,
34 UserRoleSetClb,
35 UserVpnLinkClb,
36)
37
38router = Router(name="users")
39PAGE_SIZE = 5
40ROLE_ICON = {
41 UserRole.ADMIN: "👑",
42 UserRole.REGULAR: "👤",
43}
44ROLE_NAME = {
45 UserRole.ADMIN: "Администратор",
46 UserRole.REGULAR: "Пользователь",
47}
48
49LIST_TEXT = "Список пользователей:"
50ADD_CANCEL_BUTTON = "Отменить добавление"
51EDIT_CANCEL_BUTTON = "Отменить изменение"
52
53
54class AddUserStates(StatesGroup):
55 user_id = State()
56 vpn_link = State()
57
58
59class AddUserData(BaseModel):
60 user_id: int | None = None
61
62
63class EditVpnLinkStates(StatesGroup):
64 vpn_link = State()
65
66
67class EditVpnLinkData(BaseModel):
68 page: int
69 user_id: int
70
71
72async 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
124def 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
156def 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
189def 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
212async 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"))
222async 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())
230async 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())
245async 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())
265async 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())
289async 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())
318async 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())
344async 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())
373async 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
405async 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)
414async 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)
435async 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())
464async 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)
494async 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)
513async 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)
534async 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()