first commit
This commit is contained in:
0
bot/handlers/__init__.py
Normal file
0
bot/handlers/__init__.py
Normal file
0
bot/handlers/admin/__init__.py
Normal file
0
bot/handlers/admin/__init__.py
Normal file
221
bot/handlers/admin/blacklist.py
Normal file
221
bot/handlers/admin/blacklist.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# Aiogram
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
# Const
|
||||
from create_bot import orm
|
||||
|
||||
# Keyboards
|
||||
from keyboards.admin.main_kbs import *
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates, AdminBlacklistStates
|
||||
|
||||
# Another
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
# Init
|
||||
admin_blacklist_router = Router()
|
||||
|
||||
|
||||
@admin_blacklist_router.message(
|
||||
F.text == "🚫 Черный список", StateFilter(AdminStates.main)
|
||||
)
|
||||
@admin_blacklist_router.message(F.text == "↩️ Назад", StateFilter(AdminBlacklistStates))
|
||||
async def cmd_blacklist(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "🚫 Выберите действие:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_blacklist_kb())
|
||||
|
||||
await state.set_state(AdminBlacklistStates.main)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# ADD #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_blacklist_router.message(
|
||||
F.text == "➕ Добавить", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
async def cmd_blacklist_add(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = f"➕ Введите User ID:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminBlacklistStates.add_blacklist)
|
||||
|
||||
|
||||
@admin_blacklist_router.message(F.text, StateFilter(AdminBlacklistStates.add_blacklist))
|
||||
async def cmd_blacklist_add_finish(message: types.Message, state: FSMContext):
|
||||
|
||||
# validation
|
||||
if not message.text.isdigit():
|
||||
await message.answer(
|
||||
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
|
||||
)
|
||||
return
|
||||
|
||||
user_id = int(message.text)
|
||||
|
||||
if not await orm.is_user_exists(user_id):
|
||||
await message.answer(
|
||||
text="⛔️ Пользователь не существует в БД! Повторите попытку:",
|
||||
reply_markup=get_back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
await orm.create_blacklist(user_id=user_id)
|
||||
|
||||
await message.answer(text=f"✅ Черный список обновлен!")
|
||||
await cmd_blacklist(message, state)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# DEL #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_blacklist_router.message(
|
||||
F.text == "➖ Удалить", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
async def cmd_blacklist_delete(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "➖ Введите User ID:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminBlacklistStates.del_blacklist)
|
||||
|
||||
|
||||
@admin_blacklist_router.message(F.text, StateFilter(AdminBlacklistStates.del_blacklist))
|
||||
async def cmd_blacklist_delete_finish(message: types.Message, state: FSMContext):
|
||||
|
||||
# validation
|
||||
if not message.text.isdigit():
|
||||
await message.answer(
|
||||
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
|
||||
)
|
||||
return
|
||||
|
||||
user_id = int(message.text)
|
||||
|
||||
if not await orm.is_blacklisted(user_id):
|
||||
await message.answer(
|
||||
text="⛔️ Пользователь не найден в ЧС! Повторите попытку:",
|
||||
reply_markup=get_back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
await orm.delete_blacklist(user_id=user_id)
|
||||
|
||||
await message.answer(text=f"✅ Черный список обновлен!")
|
||||
|
||||
await cmd_blacklist(message, state)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# LIST #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_blacklist_router.message(
|
||||
F.text == "👁 Открыть список", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
async def cmd_blacklist_list(message: types.Message, state: FSMContext):
|
||||
|
||||
await state.update_data(blacklist_offset=0)
|
||||
items = await orm.get_all_blacklist()
|
||||
|
||||
if not items:
|
||||
await message.answer(text="💭 Список пуст.")
|
||||
return
|
||||
|
||||
offset = 0
|
||||
max_offset = len(items) // 10 + (1 if len(items) % 10 != 0 else 0)
|
||||
|
||||
msg_text = f"<b>🚫 Черный список {offset + 1}/{max_offset}</b>\n\n"
|
||||
|
||||
for item in items[offset * 10 : (offset + 1) * 10]:
|
||||
msg_text += f"✦ <code>{item}</code>\n"
|
||||
|
||||
await message.answer(
|
||||
text=msg_text,
|
||||
reply_markup=get_bookList_ikb(
|
||||
prefix="admin_blacklist",
|
||||
offset=0,
|
||||
max_offset=max_offset,
|
||||
items=[],
|
||||
element_col=10,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def cmd_blacklist_list_query(query: types.CallbackQuery, state: FSMContext):
|
||||
|
||||
data = await state.get_data()
|
||||
offset = data.get("blacklist_offset")
|
||||
items = await orm.get_all_blacklist()
|
||||
|
||||
if not items:
|
||||
await query.answer(text="💭 Список пуст.")
|
||||
return
|
||||
|
||||
max_offset = len(items) // 10 + (1 if len(items) % 10 != 0 else 0)
|
||||
|
||||
if offset < 0:
|
||||
offset = max_offset - 1
|
||||
await state.update_data(blacklist_offset=offset)
|
||||
elif offset >= max_offset:
|
||||
offset = 0
|
||||
await state.update_data(blacklist_offset=offset)
|
||||
|
||||
msg_text = f"<b>🚫 Черный список {offset + 1}/{max_offset}</b>\n\n"
|
||||
|
||||
for item in items[offset * 10 : (offset + 1) * 10]:
|
||||
msg_text += f"✦ <code>{item}</code>\n"
|
||||
|
||||
with suppress(TelegramBadRequest):
|
||||
await query.message.edit_text(
|
||||
text=msg_text,
|
||||
reply_markup=get_bookList_ikb(
|
||||
prefix="admin_blacklist",
|
||||
offset=offset,
|
||||
max_offset=max_offset,
|
||||
items=[],
|
||||
element_col=10,
|
||||
),
|
||||
)
|
||||
|
||||
await query.answer()
|
||||
|
||||
|
||||
@admin_blacklist_router.callback_query(
|
||||
F.data == "admin_blacklist_next", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
@admin_blacklist_router.callback_query(
|
||||
F.data == "admin_blacklist_prev", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
@admin_blacklist_router.callback_query(
|
||||
F.data == "admin_blacklist_status", StateFilter(AdminBlacklistStates.main)
|
||||
)
|
||||
async def cmd_blacklist_list_actions(query: types.CallbackQuery, state: FSMContext):
|
||||
|
||||
state_data = await state.get_data()
|
||||
|
||||
if query.data.endswith("next"):
|
||||
await state.update_data(
|
||||
blacklist_offset=state_data.get("blacklist_offset", 0) + 1
|
||||
)
|
||||
elif query.data.endswith("prev"):
|
||||
await state.update_data(
|
||||
blacklist_offset=state_data.get("blacklist_offset", 0) - 1
|
||||
)
|
||||
|
||||
await cmd_blacklist_list_query(query, state)
|
||||
53
bot/handlers/admin/list_of_users.py
Normal file
53
bot/handlers/admin/list_of_users.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Aiogram
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
|
||||
# Const
|
||||
from create_bot import tz, orm
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates
|
||||
|
||||
# Another
|
||||
import shutil, os
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
# Init
|
||||
list_of_users_router = Router()
|
||||
|
||||
|
||||
@list_of_users_router.message(
|
||||
F.text == "📑 Список пользователей", StateFilter(AdminStates.main)
|
||||
)
|
||||
async def cmd_list_of_users(message: types.Message, state: FSMContext):
|
||||
|
||||
# copy the table
|
||||
table_path = shutil.copy(
|
||||
src="templates/users.xlsx", dst=f"templates/users_list.xlsx"
|
||||
)
|
||||
|
||||
# load table
|
||||
book = load_workbook(filename=table_path)
|
||||
sheet = book["users"]
|
||||
|
||||
all_clients = await orm.get_all_users()
|
||||
|
||||
for row, user in enumerate(all_clients, 2):
|
||||
sheet.cell(row=row, column=1, value=user.user_id)
|
||||
sheet.cell(row=row, column=2, value=user.username)
|
||||
sheet.cell(row=row, column=3, value=user.fullname)
|
||||
sheet.cell(
|
||||
row=row,
|
||||
column=4,
|
||||
value=user.register_date.astimezone(tz).strftime(r"%d-%m-%y %H:%M %Z"),
|
||||
)
|
||||
|
||||
book.save(table_path)
|
||||
|
||||
await message.answer_document(document=types.FSInputFile(table_path))
|
||||
|
||||
if os.path.exists(table_path):
|
||||
os.remove(table_path)
|
||||
129
bot/handlers/admin/mailer.py
Normal file
129
bot/handlers/admin/mailer.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Aiogram imports
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
|
||||
# Const
|
||||
from create_bot import bot, orm
|
||||
|
||||
# Keyboards
|
||||
from keyboards.admin.mailer_kbs import *
|
||||
|
||||
# Utils
|
||||
from utils.text_tools import parse_links_to_inline_markup
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates, AdminMailerStates
|
||||
|
||||
# Funcs
|
||||
from handlers.admin.main import show_admin_menu
|
||||
|
||||
|
||||
admin_mailer_router = Router()
|
||||
|
||||
|
||||
@admin_mailer_router.message(F.text == "✉️ Рассылка", StateFilter(AdminStates.main))
|
||||
@admin_mailer_router.message(F.text == "↩️ Назад", StateFilter(AdminMailerStates))
|
||||
async def process_mailer_post(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "✉️ Отправьте пост одним сообщением:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_back_to_main_kb())
|
||||
|
||||
await state.set_state(AdminMailerStates.post)
|
||||
|
||||
|
||||
@admin_mailer_router.message(StateFilter(AdminMailerStates.post))
|
||||
async def process_mailer_ikb(message: types.Message, state: FSMContext):
|
||||
|
||||
await state.update_data(admin_mailer_post=message.message_id)
|
||||
|
||||
msg_text = """✉️ Введите кнопки:
|
||||
|
||||
<blockquote>Отправьте ссылку(и) в формате:
|
||||
[Текст кнопки + ссылка]
|
||||
Пример:
|
||||
[Переводчик + https://t.me/TransioBot]
|
||||
|
||||
Чтобы добавить несколько кнопок в один ряд, пишите ссылки рядом с предыдущими.
|
||||
Формат:
|
||||
[Первый текст + первая ссылка][Второй текст + вторая ссылка]
|
||||
|
||||
Чтобы добавить несколько кнопок в строчку, пишите новые ссылки с новой строки.
|
||||
Формат:
|
||||
[Первый текст + первая ссылка]
|
||||
[Второй текст + вторая ссылка]</blockquote>"""
|
||||
|
||||
await message.answer(
|
||||
text=msg_text, reply_markup=get_skip_kb(), disable_web_page_preview=True
|
||||
)
|
||||
|
||||
await state.set_state(AdminMailerStates.ikb)
|
||||
|
||||
|
||||
@admin_mailer_router.message(F.text, StateFilter(AdminMailerStates.ikb))
|
||||
async def process_mailer_preview(message: types.Message, state: FSMContext):
|
||||
|
||||
ikb = (
|
||||
parse_links_to_inline_markup(message.text)
|
||||
if message.text != "↪️ Пропустить"
|
||||
else None
|
||||
)
|
||||
await state.update_data(admin_mailer_ikb=ikb)
|
||||
|
||||
state_data = await state.get_data()
|
||||
post = state_data.get("admin_mailer_post")
|
||||
|
||||
await message.answer(text="✉️ Предпросмотр:", reply_markup=get_mailer_finish_kb())
|
||||
|
||||
try:
|
||||
await bot.copy_message(
|
||||
chat_id=message.from_user.id,
|
||||
from_chat_id=message.from_user.id,
|
||||
message_id=post,
|
||||
reply_markup=get_mailer_btn_ikb(buttons_preset=ikb),
|
||||
)
|
||||
except:
|
||||
await message.answer(text="🔴 Ошибка!")
|
||||
await process_mailer_post(message, state)
|
||||
return
|
||||
|
||||
await state.set_state(AdminMailerStates.preview)
|
||||
|
||||
|
||||
@admin_mailer_router.message(
|
||||
F.text == "🟢 Начать рассылку", StateFilter(AdminMailerStates.preview)
|
||||
)
|
||||
async def process_mailer_finish(message: types.Message, state: FSMContext):
|
||||
|
||||
state_data = await state.get_data()
|
||||
ikb = state_data.get("admin_mailer_ikb")
|
||||
post = state_data.get("admin_mailer_post")
|
||||
|
||||
all_users = await orm.get_all_user_ids()
|
||||
|
||||
# info
|
||||
await message.answer(text="▶️✉️ Рассылка запущена...")
|
||||
|
||||
await state.clear()
|
||||
|
||||
# back to main menu
|
||||
await show_admin_menu(message, state)
|
||||
|
||||
counter = 0
|
||||
for user_id in all_users:
|
||||
try:
|
||||
await bot.copy_message(
|
||||
chat_id=user_id,
|
||||
from_chat_id=message.from_user.id,
|
||||
message_id=post,
|
||||
reply_markup=get_mailer_btn_ikb(buttons_preset=ikb),
|
||||
)
|
||||
counter += 1
|
||||
except:
|
||||
pass
|
||||
|
||||
await message.answer(
|
||||
text=f"✅ Рассылка завершена! Сообщение отправлено {counter}/{len(all_users)}."
|
||||
)
|
||||
67
bot/handlers/admin/main.py
Normal file
67
bot/handlers/admin/main.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Aiogram
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram import Router, F
|
||||
|
||||
# Const
|
||||
from create_bot import orm
|
||||
|
||||
# Keyboards
|
||||
from keyboards.admin.main_kbs import *
|
||||
|
||||
# States
|
||||
from states.admin_states import (
|
||||
AdminStates,
|
||||
AdminMailerStates,
|
||||
AdminManagementStates,
|
||||
AdminSettingsStates,
|
||||
AdminBlacklistStates,
|
||||
)
|
||||
|
||||
# Funcs
|
||||
from handlers.start import cmd_start
|
||||
|
||||
|
||||
# Init
|
||||
admin_main_router = Router()
|
||||
|
||||
|
||||
@admin_main_router.message(Command("admin"), StateFilter("*"))
|
||||
async def cmd_login_as_admin(message: types.Message, state: FSMContext):
|
||||
|
||||
if message.chat.type != "private":
|
||||
return
|
||||
|
||||
is_admin_exists = await orm.is_admin_exists(user_id=message.from_user.id)
|
||||
|
||||
if is_admin_exists:
|
||||
await show_admin_menu(message, state)
|
||||
else:
|
||||
await message.answer(text="🤨")
|
||||
|
||||
|
||||
@admin_main_router.message(F.text == "🔚 Выйти", StateFilter(AdminStates.main))
|
||||
async def cmd_admin_exit(message: types.Message, state: FSMContext):
|
||||
|
||||
await message.answer(text="🚪⠀", reply_markup=types.ReplyKeyboardRemove())
|
||||
|
||||
await cmd_start(message, state)
|
||||
|
||||
|
||||
@admin_main_router.message(
|
||||
F.text == "↩️ Вернуться в меню",
|
||||
StateFilter(
|
||||
AdminManagementStates.main,
|
||||
AdminMailerStates.post,
|
||||
AdminSettingsStates.main,
|
||||
AdminBlacklistStates.main,
|
||||
),
|
||||
)
|
||||
async def show_admin_menu(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "👮♂️ Вы находитесь в админ-панели"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_main_menu_kb())
|
||||
|
||||
await state.set_state(AdminStates.main)
|
||||
142
bot/handlers/admin/management.py
Normal file
142
bot/handlers/admin/management.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Aiogram imports
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
# Const
|
||||
from create_bot import bot, storage, StorageKey, orm
|
||||
|
||||
# Keyboards
|
||||
from keyboards.admin.main_kbs import *
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates, AdminManagementStates
|
||||
|
||||
# Config
|
||||
from decouple import config
|
||||
|
||||
# Another
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
# Init
|
||||
admin_management_router = Router()
|
||||
|
||||
|
||||
@admin_management_router.message(
|
||||
F.text == "👮♂️ Управление админами", StateFilter(AdminStates.main)
|
||||
)
|
||||
@admin_management_router.message(
|
||||
F.text == "↩️ Назад", StateFilter(AdminManagementStates)
|
||||
)
|
||||
async def cmd_management(message: types.Message, state: FSMContext):
|
||||
|
||||
admins = await orm.get_all_admins()
|
||||
|
||||
msg_text = "<i>👮♂️ Действующие администраторы</i>\n"
|
||||
|
||||
for admin in admins:
|
||||
msg_text += f"✦ [<code>{admin.user_id}</code>]: {admin.username if admin.username else admin.fullname}\n"
|
||||
|
||||
msg_text += f"\n<b>🔽 Выберите действие:</b>"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_add_admins_kb())
|
||||
|
||||
await state.set_state(AdminManagementStates.main)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# ADD #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_management_router.message(
|
||||
F.text == "➕ Добавить", StateFilter(AdminManagementStates.main)
|
||||
)
|
||||
async def cmd_management_add_id(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "➕ Введите User ID нового админа:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminManagementStates.add_admin)
|
||||
|
||||
|
||||
@admin_management_router.message(F.text, StateFilter(AdminManagementStates.add_admin))
|
||||
async def cmd_management_add_finish(message: types.Message, state: FSMContext):
|
||||
|
||||
# validation
|
||||
if not message.text.isdigit():
|
||||
await message.answer(
|
||||
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
|
||||
)
|
||||
return
|
||||
|
||||
user_id = int(message.text)
|
||||
|
||||
if not await orm.is_user_exists(user_id):
|
||||
await message.answer(
|
||||
text="⛔️ Пользователь не существует в БД! Повторите попытку:",
|
||||
reply_markup=get_back_kb(),
|
||||
)
|
||||
return
|
||||
|
||||
user = await orm.get_user(user_id)
|
||||
await orm.create_admin(user.user_id, user.username, user.fullname)
|
||||
await message.answer("✅ Успешно!")
|
||||
await cmd_management(message, state)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# DELETE #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_management_router.message(
|
||||
F.text == "➖ Удалить", StateFilter(AdminManagementStates.main)
|
||||
)
|
||||
async def cmd_management_delete(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "➖ Введите ID админа для удаления:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminManagementStates.del_admin)
|
||||
|
||||
|
||||
@admin_management_router.message(F.text, StateFilter(AdminManagementStates.del_admin))
|
||||
async def cmd_management_delete_finish(message: types.Message, state: FSMContext):
|
||||
|
||||
# validation
|
||||
if not message.text.isdigit():
|
||||
await message.answer(text="⛔️ Только цифры! Повторите попытку:")
|
||||
return
|
||||
|
||||
user_id = int(message.text)
|
||||
|
||||
if user_id == int(config("BASE_ADMIN")):
|
||||
await message.answer(
|
||||
text="⛔️ Отказано! Повторите попытку:", reply_markup=get_back_kb()
|
||||
)
|
||||
return
|
||||
|
||||
if not await orm.is_admin_exists(user_id):
|
||||
await message.answer(text="⛔️ Админ не найден! Повторите попытку:")
|
||||
return
|
||||
|
||||
# change admin state
|
||||
with suppress(TelegramBadRequest):
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text="☹️ Вы больше не являетесь админом!",
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
await storage.set_state(
|
||||
key=StorageKey(bot_id=bot.id, chat_id=user_id, user_id=user_id), state=None
|
||||
)
|
||||
await orm.delete_admin(user_id)
|
||||
await message.answer("✅ Успешно!")
|
||||
await cmd_management(message, state)
|
||||
85
bot/handlers/admin/settings.py
Normal file
85
bot/handlers/admin/settings.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Aiogram imports
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
|
||||
# Const
|
||||
from create_bot import orm
|
||||
|
||||
# Keyboards
|
||||
from keyboards.admin.main_kbs import *
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates, AdminSettingsStates
|
||||
|
||||
|
||||
# Init
|
||||
admin_settings_router = Router()
|
||||
|
||||
|
||||
@admin_settings_router.message(F.text == "↩️ Назад", StateFilter(AdminSettingsStates))
|
||||
@admin_settings_router.message(F.text == "⚙️ Настройки", StateFilter(AdminStates.main))
|
||||
async def cmd_settings(message: types.Message, state: FSMContext):
|
||||
|
||||
msg_text = "⚙️ Выберите, что хотите изменить:"
|
||||
|
||||
await message.answer(text=msg_text, reply_markup=get_settings_kb())
|
||||
|
||||
await state.set_state(AdminSettingsStates.main)
|
||||
|
||||
|
||||
# *############################
|
||||
# *# EDIT PHOTO #
|
||||
# *############################
|
||||
|
||||
|
||||
@admin_settings_router.message(
|
||||
F.text.in_({"🖼 ..."}), StateFilter(AdminSettingsStates.main)
|
||||
)
|
||||
async def cmd_edit_photo(message: types.Message, state: FSMContext):
|
||||
|
||||
x = {"🖼 ...": "..."}
|
||||
|
||||
setting_key = x.get(message.text)
|
||||
await state.update_data(setting_key=setting_key)
|
||||
photo = await orm.get_setting_value(setting_key)
|
||||
|
||||
msg_text = f"""<b>Текущее значение:</b>
|
||||
<blockquote>{photo}</blockquote>
|
||||
|
||||
⌨️ Отправьте фото для изменения:"""
|
||||
|
||||
if photo:
|
||||
await message.answer_photo(
|
||||
photo=photo, caption=msg_text, reply_markup=get_back_kb()
|
||||
)
|
||||
else:
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminSettingsStates.edit_photo)
|
||||
|
||||
|
||||
@admin_settings_router.message(F.photo, StateFilter(AdminSettingsStates.edit_photo))
|
||||
async def cmd_edit_photo_setup(message: types.Message, state: FSMContext):
|
||||
|
||||
photo = message.photo[-1].file_id
|
||||
|
||||
state_data = await state.get_data()
|
||||
setting_key = state_data.get("setting_key")
|
||||
|
||||
await orm.update_setting_value(setting_key, photo)
|
||||
|
||||
msg_text = f"""<b>Текущее значение:</b>
|
||||
<blockquote>{photo}</blockquote>
|
||||
|
||||
⌨️ Отправьте фото для изменения:"""
|
||||
|
||||
if photo:
|
||||
await message.answer_photo(
|
||||
photo=photo, caption=msg_text, reply_markup=get_back_kb()
|
||||
)
|
||||
else:
|
||||
await message.answer(text=msg_text, reply_markup=get_back_kb())
|
||||
|
||||
await state.set_state(AdminSettingsStates.edit_photo)
|
||||
28
bot/handlers/admin/statistic.py
Normal file
28
bot/handlers/admin/statistic.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Aiogram
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram import Router, F
|
||||
|
||||
# Const
|
||||
from create_bot import orm
|
||||
|
||||
# States
|
||||
from states.admin_states import AdminStates
|
||||
|
||||
# Init
|
||||
admin_statistic_router = Router()
|
||||
|
||||
|
||||
@admin_statistic_router.message(
|
||||
F.text == "📊 Статистика", StateFilter(AdminStates.main)
|
||||
)
|
||||
async def cmd_statistic(message: types.Message, state: FSMContext):
|
||||
|
||||
users_count = await orm.get_users_count()
|
||||
|
||||
msg_text = f"""<i>📊 Статистика</i>
|
||||
|
||||
🔹 Кол-во пользователей в боте: {users_count:,} чел."""
|
||||
|
||||
await message.answer(text=msg_text)
|
||||
0
bot/handlers/client/__init__.py
Normal file
0
bot/handlers/client/__init__.py
Normal file
116
bot/handlers/client/payments.py
Normal file
116
bot/handlers/client/payments.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
import aiogram.types as types
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import StateFilter
|
||||
|
||||
from create_bot import bot, orm
|
||||
from keyboards.inline_keyboards import get_pay_link_kb
|
||||
from services.wata import WataAPIError, WataClient, build_telegram_payment_return_url
|
||||
from states.client_states import MainStates
|
||||
from utils.amount_parser import format_rub_amount, parse_rub_amount
|
||||
|
||||
|
||||
payment_router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
wata_client = WataClient()
|
||||
|
||||
|
||||
@payment_router.message(StateFilter(MainStates.waiting_amount))
|
||||
async def create_payment_link(message: types.Message, state: FSMContext):
|
||||
if not message.text:
|
||||
await message.answer("Отправьте сумму текстом. Например: 1000 рублей.")
|
||||
return
|
||||
|
||||
amount = parse_rub_amount(message.text)
|
||||
if amount is None:
|
||||
await message.answer(
|
||||
"Не удалось распознать сумму. Напишите её в рублях, например: 1000 или 1000 рублей."
|
||||
)
|
||||
return
|
||||
|
||||
bot_username = (await bot.get_me()).username
|
||||
order_id = f"tg-{message.from_user.id}-{uuid4().hex[:12]}"
|
||||
description = f"WechatPayBot payment {order_id}"
|
||||
|
||||
await orm.create_payment(
|
||||
user_id=message.from_user.id,
|
||||
order_id=order_id,
|
||||
amount=amount,
|
||||
currency="RUB",
|
||||
description=description,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
try:
|
||||
payment_link = await wata_client.create_payment_link(
|
||||
amount=amount,
|
||||
order_id=order_id,
|
||||
description=description,
|
||||
success_redirect_url=build_telegram_payment_return_url(
|
||||
bot_username=bot_username,
|
||||
order_id=order_id,
|
||||
success=True,
|
||||
),
|
||||
fail_redirect_url=build_telegram_payment_return_url(
|
||||
bot_username=bot_username,
|
||||
order_id=order_id,
|
||||
success=False,
|
||||
),
|
||||
)
|
||||
except WataAPIError as exc:
|
||||
logger.exception("Failed to create WATA payment link for order %s", order_id)
|
||||
await orm.update_payment_status(
|
||||
order_id=order_id,
|
||||
status="error",
|
||||
transaction_status=None,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
error_description=exc.message,
|
||||
)
|
||||
await message.answer(
|
||||
f"Не удалось создать ссылку на оплату.\nПричина: {exc.message}"
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Unexpected error while creating WATA payment link")
|
||||
await orm.update_payment_status(
|
||||
order_id=order_id,
|
||||
status="error",
|
||||
transaction_status=None,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
error_description="Internal error while creating payment link",
|
||||
)
|
||||
await message.answer(
|
||||
"Не удалось создать ссылку на оплату из-за внутренней ошибки. Попробуйте позже."
|
||||
)
|
||||
return
|
||||
|
||||
await orm.update_payment_link(
|
||||
order_id=order_id,
|
||||
payment_link_id=payment_link.id,
|
||||
payment_url=payment_link.url,
|
||||
payment_link_status=payment_link.status,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Payment link created",
|
||||
extra={
|
||||
"user_id": message.from_user.id,
|
||||
"order_id": order_id,
|
||||
"amount": str(amount),
|
||||
"payment_link_id": payment_link.id,
|
||||
},
|
||||
)
|
||||
|
||||
await state.set_state(MainStates.main)
|
||||
await message.answer(
|
||||
text=(
|
||||
f"Сумма к оплате: {format_rub_amount(amount)}\n\n"
|
||||
"Нажмите кнопку ниже, чтобы перейти к безопасной оплате."
|
||||
),
|
||||
reply_markup=get_pay_link_kb(payment_link.url),
|
||||
)
|
||||
167
bot/handlers/start.py
Normal file
167
bot/handlers/start.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Aiogram
|
||||
import aiogram.types as types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import CommandObject, CommandStart, StateFilter
|
||||
from aiogram import Router
|
||||
|
||||
# Utils
|
||||
from utils.text_tools import to_html
|
||||
from utils.amount_parser import format_rub_amount
|
||||
|
||||
# Const
|
||||
from create_bot import orm
|
||||
from services.wata import WataAPIError, WataClient
|
||||
|
||||
# States
|
||||
from states.client_states import MainStates
|
||||
|
||||
# Another
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
|
||||
# Init
|
||||
start_router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
wata_client = WataClient()
|
||||
|
||||
|
||||
@start_router.message(CommandStart(), StateFilter("*"))
|
||||
async def cmd_start(
|
||||
message: types.Message, state: FSMContext, command: CommandObject | None = None
|
||||
):
|
||||
|
||||
if message.chat.type != "private":
|
||||
return
|
||||
|
||||
await register_user(message)
|
||||
|
||||
command_args = command.args if command else None
|
||||
if command_args and command_args.startswith("payment_failed_"):
|
||||
order_id = command_args.removeprefix("payment_failed_")
|
||||
await show_payment_result(message, state, order_id)
|
||||
return
|
||||
|
||||
if command_args and command_args.startswith("payment_"):
|
||||
order_id = command_args.removeprefix("payment_")
|
||||
await show_payment_result(message, state, order_id)
|
||||
return
|
||||
|
||||
await state.set_state(MainStates.waiting_amount)
|
||||
await message.answer(
|
||||
text=(
|
||||
"Для продолжения работы укажите сумму оплаты в рублях, "
|
||||
"и я отправлю ссылку для безопасной оплаты.\n\n"
|
||||
"Отправьте сумму сообщением."
|
||||
),
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
async def register_user(message: types.Message) -> None:
|
||||
user_id = message.from_user.id
|
||||
username = (
|
||||
"@" + message.from_user.username
|
||||
if message.from_user.username is not None
|
||||
else None
|
||||
)
|
||||
fullname = to_html(message.from_user.full_name)
|
||||
|
||||
await orm.create_user(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
fullname=fullname,
|
||||
register_date=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
async def show_payment_result(
|
||||
message: types.Message, state: FSMContext, order_id: str
|
||||
) -> None:
|
||||
payment = await orm.get_payment_by_order_id(order_id)
|
||||
if payment is None:
|
||||
await state.set_state(MainStates.waiting_amount)
|
||||
await message.answer(
|
||||
text=(
|
||||
"Платёж не найден.\n\n"
|
||||
"Отправьте сумму сообщением, чтобы создать новую ссылку."
|
||||
),
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
return
|
||||
|
||||
if payment.status not in {"paid", "declined"}:
|
||||
await sync_payment_status(order_id)
|
||||
payment = await orm.get_payment_by_order_id(order_id)
|
||||
|
||||
await state.set_state(MainStates.main)
|
||||
|
||||
if payment.status == "paid":
|
||||
await message.answer(
|
||||
text=(
|
||||
f"Спасибо за оплату!\n\n"
|
||||
f"Платёж на сумму {format_rub_amount(payment.amount)} подтверждён."
|
||||
),
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
return
|
||||
|
||||
if payment.status == "declined":
|
||||
error_text = ""
|
||||
if payment.error_description:
|
||||
error_text = f"\nПричина: {payment.error_description}"
|
||||
|
||||
await message.answer(
|
||||
text=(
|
||||
"Оплата не была подтверждена платёжным сервисом."
|
||||
f"{error_text}\n\n"
|
||||
"Отправьте новую сумму сообщением, чтобы попробовать ещё раз."
|
||||
),
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
text=(
|
||||
"Платёж ещё обрабатывается платёжным сервисом. "
|
||||
"Если вы уже оплатили, подождите несколько секунд и снова откройте бота."
|
||||
),
|
||||
reply_markup=types.ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
async def sync_payment_status(order_id: str) -> None:
|
||||
try:
|
||||
transaction = await wata_client.find_transaction_by_order_id(order_id)
|
||||
except WataAPIError:
|
||||
logger.exception("WATA returned an API error while syncing order %s", order_id)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Unexpected error while syncing order %s", order_id)
|
||||
return
|
||||
|
||||
if transaction is None:
|
||||
return
|
||||
|
||||
paid_at = None
|
||||
if transaction.payment_time:
|
||||
paid_at = datetime.fromisoformat(
|
||||
transaction.payment_time.strip().replace("Z", "+00:00")
|
||||
)
|
||||
|
||||
local_status = "pending"
|
||||
if transaction.status == "Paid":
|
||||
local_status = "paid"
|
||||
elif transaction.status == "Declined":
|
||||
local_status = "declined"
|
||||
|
||||
await orm.update_payment_status(
|
||||
order_id=order_id,
|
||||
status=local_status,
|
||||
transaction_status=transaction.status,
|
||||
transaction_id=transaction.id,
|
||||
error_code=transaction.error_code,
|
||||
error_description=transaction.error_description,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
paid_at=paid_at,
|
||||
)
|
||||
Reference in New Issue
Block a user