Merge pull request #81 from izzzzzi/main

Add crypto(beta), fix import, fix requirements.txt
This commit is contained in:
Vladislav Lisitsyn
2024-11-11 22:25:45 +03:00
committed by GitHub
13 changed files with 614 additions and 329 deletions
+4 -5
View File
@@ -1,8 +1,7 @@
from aiogram import Bot, Dispatcher, Router
from aiogram.fsm.storage.memory import MemoryStorage
from config import API_TOKEN, FREEKASSA_ENABLE, YOOKASSA_ENABLE
from middlewares.admin import AdminMiddleware
from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, YOOKASSA_ENABLE
from middlewares.logging import UserActivityMiddleware
bot = Bot(token=API_TOKEN)
@@ -13,7 +12,7 @@ router = Router()
from handlers import commands, notifications, profile, start
from handlers.admin import admin, admin_panel, user_editor
from handlers.keys import key_management, keys
from handlers.payment import freekassa_pay, yookassa_pay
from handlers.payment import cryprobot_pay, freekassa_pay, yookassa_pay
dp.include_router(admin.router)
dp.include_router(admin_panel.router)
@@ -27,9 +26,9 @@ if YOOKASSA_ENABLE:
dp.include_router(yookassa_pay.router)
if FREEKASSA_ENABLE:
dp.include_router(freekassa_pay.router)
if CRYPTO_BOT_ENABLE:
dp.include_router(cryprobot_pay.router)
dp.include_router(notifications.router)
dp.message.middleware(AdminMiddleware())
dp.callback_query.middleware(AdminMiddleware())
dp.message.middleware(UserActivityMiddleware())
dp.callback_query.middleware(UserActivityMiddleware())
+17 -17
View File
@@ -1,27 +1,27 @@
from aiogram import Router, types
from aiogram.filters import Command
from filters.admin import IsAdminFilter
from database import add_balance_to_client, check_connection_exists
router = Router()
@router.message(Command("add_balance"))
async def cmd_add_balance(message: types.Message, is_admin: bool):
if is_admin:
try:
_, client_id, amount = message.text.split()
amount = float(amount)
@router.message(Command("add_balance"), IsAdminFilter())
async def cmd_add_balance(message: types.Message):
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"Клиент с ID {client_id} не найден.")
return
if not await check_connection_exists(int(client_id)):
await message.reply(f"Клиент с ID {client_id} не найден.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.")
except ValueError:
await message.reply(
"Пожалуйста, используйте формат: /add_balance <client_id> <amount>"
)
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
await add_balance_to_client(int(client_id), amount)
await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.")
except ValueError:
await message.reply(
"Пожалуйста, используйте формат: /add_balance <client_id> <amount>"
)
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
+122 -140
View File
@@ -8,6 +8,7 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from filters.admin import IsAdminFilter
from backup import backup_database
from bot import bot
@@ -22,162 +23,143 @@ class UserEditorState(StatesGroup):
displaying_user_info = State()
@router.message(Command("admin"))
async def handle_admin_command(message: types.Message, is_admin: bool):
if is_admin:
@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_command(message: types.Message):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам", callback_data="send_to_alls"
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
message.chat.id, "Панель администратора.", reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"🔹 <b>Общая статистика пользователей:</b>\n"
f"• Всего пользователей: <b>{total_users}</b>\n"
f"• Всего ключей: <b>{total_keys}</b>\n"
f"• Всего рефералов: <b>{total_referrals}</b>\n"
f"• Активные ключи: <b>{active_keys}</b>\n"
f"• Истекшие ключи: <b>{expired_keys}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="Редактор пользователей", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам", callback_data="send_to_alls"
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
message.chat.id, "Панель администратора.", reply_markup=builder.as_markup()
)
else:
await bot.send_message(message.chat.id, "У вас нет доступа к этой команде.")
@router.callback_query(F.data == "user_stats")
async def user_stats_menu(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
conn = await asyncpg.connect(DATABASE_URL)
try:
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"🔹 <b>Общая статистика пользователей:</b>\n"
f"• Всего пользователей: <b>{total_users}</b>\n"
f"• Всего ключей: <b>{total_keys}</b>\n"
f"• Всего рефералов: <b>{total_referrals}</b>\n"
f"• Активные ключи: <b>{active_keys}</b>\n"
f"• Истекшие ключи: <b>{expired_keys}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
)
await callback_query.message.edit_text(
stats_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(F.data == "send_to_alls")
async def handle_send_to_all(
callback_query: CallbackQuery, state: FSMContext, is_admin: bool
):
if is_admin:
await send_message_to_all_clients(
callback_query.message, state, from_panel=True
)
await callback_query.answer()
@router.callback_query(F.data == "backups")
async def handle_backup(message: Message, is_admin: bool):
if is_admin:
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.callback_query(F.data == "restart_bot")
async def handle_restart(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await callback_query.message.answer("Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await callback_query.message.answer(
f"Бот будет перезапущен через 30 секунд {e.stderr}"
)
@router.callback_query(F.data == "user_editor")
async def user_editor_menu(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Поиск по имени ключа", callback_data="search_by_key_name"
)
)
builder.row(
InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")
)
builder.row(
InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
)
await callback_query.message.edit_text(
"Выберите метод поиска:", reply_markup=builder.as_markup()
stats_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(F.data == "send_to_alls", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await send_message_to_all_clients(callback_query.message, state, from_panel=True)
await callback_query.answer()
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(message: Message):
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.callback_query(F.data == "restart_bot", IsAdminFilter)
async def handle_restart(callback_query: CallbackQuery):
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await callback_query.message.answer("Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await callback_query.message.answer(
f"Бот будет перезапущен через 30 секунд {e.stderr}"
)
@router.callback_query(F.data == "back_to_admin_menu")
async def back_to_admin_menu(callback_query: CallbackQuery, is_admin: bool):
@router.callback_query(F.data == "user_editor", IsAdminFilter())
async def user_editor_menu(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Поиск по имени ключа", callback_data="search_by_key_name"
)
)
builder.row(
InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")
)
builder.row(InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu"))
await callback_query.message.edit_text(
"Выберите метод поиска:", reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "back_to_admin_menu", IsAdminFilter())
async def back_to_admin_menu(callback_query: CallbackQuery):
try:
await callback_query.message.delete()
except Exception:
pass
tg_id = callback_query.from_user.id
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
builder.row(
InlineKeyboardButton(
text="Редактор пользователей", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам",
callback_data="send_to_alls",
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
tg_id, "Панель администратора", reply_markup=builder.as_markup()
)
builder.row(
InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам",
callback_data="send_to_alls",
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
tg_id, "Панель администратора", reply_markup=builder.as_markup()
)
async def handle_error(tg_id, callback_query, message):
+67 -70
View File
@@ -4,6 +4,7 @@ from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
from filters.admin import IsAdminFilter
from loguru import logger
from bot import bot
@@ -25,14 +26,13 @@ class Form(StatesGroup):
waiting_for_message = State()
@router.message(Command("backup"))
async def backup_command(message: Message, is_admin: bool):
if is_admin:
from backup import backup_database
@router.message(Command("backup"), IsAdminFilter())
async def backup_command(message: Message):
from backup import backup_database
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.message(Command("start"))
@@ -40,7 +40,7 @@ async def handle_start(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command("add_balance"))
@router.message(Command("add_balance"), IsAdminFilter())
async def handle_add_balance(message: types.Message, state: FSMContext):
await cmd_add_balance(message)
@@ -50,56 +50,53 @@ async def handle_menu(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command("send_trial"))
async def handle_send_trial_command(
message: types.Message, state: FSMContext, is_admin: bool
):
if is_admin:
@router.message(Command("send_trial"), IsAdminFilter())
async def handle_send_trial_command(message: types.Message, state: FSMContext):
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
"""
)
if records:
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
logger.info(
f"Бот заблокирован пользователем с tg_id: {tg_id}"
)
else:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
"Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом."
)
else:
await message.answer(
"Нет пользователей с не использованными пробными ключами."
)
if records:
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
logger.info(
f"Бот заблокирован пользователем с tg_id: {tg_id}"
)
else:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
finally:
await conn.close()
await message.answer(
"Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом."
)
else:
await message.answer(
"Нет пользователей с не использованными пробными ключами."
)
finally:
await conn.close()
except Exception as e:
await message.answer(f"Ошибка при отправке сообщений: {e}")
except Exception as e:
await message.answer(f"Ошибка при отправке сообщений: {e}")
@router.message(Command("send_to_all"))
@router.message(Command("send_to_all"), IsAdminFilter())
async def send_message_to_all_clients(
message: types.Message, state: FSMContext, is_admin: bool, from_panel=False
message: types.Message, state: FSMContext, from_panel=False
):
if from_panel and is_admin:
if from_panel:
await message.answer(
"Введите текст сообщения, который вы хотите отправить всем клиентам:"
@@ -107,32 +104,32 @@ async def send_message_to_all_clients(
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message)
@router.message(Form.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message, state: FSMContext, is_admin: bool
message: types.Message,
state: FSMContext,
):
if is_admin:
text_message = message.text
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
except Exception as e:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя."
)
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
except Exception as e:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя."
)
await message.answer("Сообщение было отправлено всем клиентам.")
except Exception as e:
logger.error(f"Ошибка при подключении к базе данных: {e}")
await message.answer("Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await message.answer("Сообщение было отправлено всем клиентам.")
except Exception as e:
logger.error(f"Ошибка при подключении к базе данных: {e}")
await message.answer("Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
+9
View File
@@ -0,0 +1,9 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
class IsAdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
return message.from_user.id == ADMIN_ID
+1 -1
View File
@@ -9,7 +9,7 @@ from py3xui import AsyncApi
from client import delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS
from database import delete_key, get_balance, update_balance, update_key_expiry
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWAL_FAILED, KEY_RENEWED, RENEWAL_PLANS
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS
router = Router()
+239 -4
View File
@@ -1,13 +1,187 @@
from aiocryptopay import AioCryptoPay, Networks
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from loguru import logger
from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN
from bot import bot
from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.profile import process_callback_view_profile
from handlers.texts import PAYMENT_OPTIONS
router = Router()
if CRYPTO_BOT_ENABLE:
crypto = AioCryptoPay(token=CRYPTO_BOT_TOKEN, network=Networks.MAIN_NET)
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "replenish_balance")
async def process_callback_replenish_balance(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"],
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
)
)
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(F.data == "back_to_profile")
async def back_to_profile_handler(
callback_query: types.CallbackQuery, state: FSMContext
):
await process_callback_view_profile(callback_query, state)
@router.callback_query(F.data.startswith("amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
await send_message_with_deletion(
callback_query.from_user.id,
"Неверные данные для выбора суммы.",
state=state,
message_key="amount_error_message_id",
)
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(
callback_query.from_user.id,
"Некорректная сумма.",
state=state,
message_key="amount_error_message_id",
)
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
try:
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(amount // RUB_TO_USDT),
description=f"Пополнения баланса на {amount//RUB_TO_USDT} руб",
payload=f"{callback_query.from_user.id}:{amount//RUB_TO_USDT}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url),
InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"),
)
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount//RUB_TO_USDT} рублей.",
reply_markup=builder.as_markup(),
)
else:
await send_message_with_deletion(
callback_query.from_user.id, "Ошибка при создании платежа.", state=state
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def cryptobot_webhook(request):
try:
data = await request.json()
@@ -16,7 +190,9 @@ async def cryptobot_webhook(request):
await process_crypto_payment(data["payload"])
return web.Response(status=200)
else:
logger.warning(f"Неподдерживаемый тип обновления: {data.get('update_type')}")
logger.warning(
f"Неподдерживаемый тип обновления: {data.get('update_type')}"
)
return web.Response(status=400)
except Exception as e:
logger.error(f"Ошибка обработки вебхука: {e}")
@@ -26,7 +202,66 @@ async def cryptobot_webhook(request):
async def process_crypto_payment(payload):
if payload["status"] == "paid":
custom_payload = payload["payload"]
user_id, sub_type = custom_payload.split(":")
# TODO
user_id_str, amount_str = custom_payload.split(":")
try:
user_id = int(user_id_str)
amount = float(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
else:
logger.warning(f"Получен неоплаченный инвойс: {payload}")
@router.callback_query(F.data == "enter_custom_amount")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(State(ReplenishBalanceState.entering_custom_amount))
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer(
"Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
try:
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(amount // RUB_TO_USDT),
description=f"Пополнения баланса на {amount//RUB_TO_USDT} руб",
payload=f"{message.from_user.id}:{amount//RUB_TO_USDT}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url),
InlineKeyboardButton(
text="⬅️ Назад", callback_data="back_to_profile"
),
)
await message.message.edit_text(
text=f"Вы выбрали пополнение на {amount//RUB_TO_USDT} рублей.",
reply_markup=builder.as_markup(),
)
else:
await send_message_with_deletion(
message.from_user.id, "Ошибка при создании платежа.", state=state
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+22 -10
View File
@@ -6,7 +6,8 @@ import requests
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from loguru import logger
@@ -95,16 +96,27 @@ async def process_callback_pay_freekassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
inline_keyboard = []
for payment in PAYMENT_OPTIONS:
inline_keyboard.append(
InlineKeyboardButton(
text=payment.get("text"), callback_data=payment.get("callback_data")
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"],
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
)
)
)
amount_keyboard = InlineKeyboardMarkup(inline_keyboard)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
@@ -113,7 +125,7 @@ async def process_callback_pay_freekassa(
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=amount_keyboard,
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount)
+25 -52
View File
@@ -17,10 +17,9 @@ from handlers.texts import PAYMENT_OPTIONS
router = Router()
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
if YOOKASSA_ENABLE:
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
logger.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logger.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
@@ -64,39 +63,25 @@ async def process_callback_replenish_balance(
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[0]["text"],
callback_data=PAYMENT_OPTIONS[0]["callback_data"],
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[1]["text"],
callback_data=PAYMENT_OPTIONS[1]["callback_data"],
),
)
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[2]["text"],
callback_data=PAYMENT_OPTIONS[2]["callback_data"],
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[3]["text"],
callback_data=PAYMENT_OPTIONS[3]["callback_data"],
),
)
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[4]["text"],
callback_data=PAYMENT_OPTIONS[4]["callback_data"],
)
)
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[5]["text"],
callback_data=PAYMENT_OPTIONS[5]["callback_data"],
)
)
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=PAYMENT_OPTIONS[i + 1]["callback_data"],
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=PAYMENT_OPTIONS[i]["callback_data"],
)
)
key_count = await get_key_count(tg_id)
@@ -215,20 +200,15 @@ async def process_amount_selection(
async def send_payment_success_notification(user_id: int, amount: float):
try:
profile_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
]
]
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=profile_keyboard,
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
@@ -236,26 +216,19 @@ async def send_payment_success_notification(user_id: int, amount: float):
async def yookassa_webhook(request):
event = await request.json()
logger.debug(f"Webhook event received: {event}")
if event["event"] == "payment.succeeded":
user_id_str = event["object"]["metadata"]["user_id"]
amount_str = event["object"]["amount"]["value"]
try:
user_id = int(user_id_str)
amount = float(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
+1 -1
View File
@@ -11,9 +11,9 @@ from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, SUB_PATH, WEBAPP_HOST, W
from database import init_db
from handlers.keys.subscriptions import handle_subscription
from handlers.notifications import notify_expiring_keys
from handlers.payment.cryprobot_pay import cryptobot_webhook
from handlers.payment.freekassa_pay import freekassa_webhook
from handlers.payment.yookassa_pay import yookassa_webhook
from handlers.payment.cryprobot_pay import cryptobot_webhook
async def periodic_notifications():
-28
View File
@@ -1,28 +0,0 @@
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from config import ADMIN_ID
class AdminMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user_id = None
if isinstance(event, Message):
user_id = event.from_user.id
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
if user_id != int(ADMIN_ID):
data["is_admin"] = False
else:
data["is_admin"] = True
return await handler(event, data)
+105
View File
@@ -0,0 +1,105 @@
# Обработка HTTP-запросов, редирект на HTTPS
server {
listen 80;
server_name example.com; # Домен проекта
location / {
# Перенаправление с HTTP на HTTPS
if ($arg_url != "") {
return 301 $arg_url;
}
return 404 "URL-аргумент отсутствует";
}
}
# Обработка HTTPS-запросов (статический сайт и прокси)
server {
listen 443 ssl;
server_name example.com; # Домен проекта
# SSL настройки
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # Путь к SSL-сертификату
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Путь к приватному ключу
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Обработка подписок
location /sub {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Content-Type text/plain;
add_header Content-Disposition inline;
add_header Cache-Control no-store;
add_header Pragma no-cache;
}
# Статический сайт
root /var/www/website; # Путь к статическому сайту
index index.html; # Основной файл
location / {
# Перенаправление с HTTP на HTTPS
if ($arg_url != "") {
return 301 $arg_url;
}
return 404 "URL-аргумент отсутствует";
}
# Вебхуки из main.py
location /webhook {
proxy_pass http://localhost:3001/webhook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Вебхук Юкассы
location /yookassa/webhook {
proxy_pass http://localhost:3001/yookassa/webhook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Вебхук FreeCassa
location /freekassa/webhook {
proxy_pass http://localhost:3001/freekassa/webhook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Вебхук CryptoBot
location /cryptobot/webhook {
proxy_pass http://localhost:3001/cryptobot/webhook;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
+2 -1
View File
@@ -26,4 +26,5 @@ wrapt==1.16.0
yarl==1.15.5
yookassa==3.3.0
loguru
aiocryptopay
aiocryptopay
py3xui