currency/fast flow for everyone/optimization and cache/new payment system/and more

This commit is contained in:
Vladless
2025-09-15 21:55:31 +03:00
parent 6d9e1aba4a
commit d6b920ff4d
102 changed files with 3033 additions and 1710 deletions
+6 -3
View File
@@ -1,7 +1,10 @@
formatting: format:
@echo "Running Ruff format..." && ruff format . --config pyproject.toml --exclude main.py,handlers/payments @echo "Running Ruff format..." && ruff format . --config pyproject.toml --exclude main.py,handlers/payments
@echo "Running Ruff..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix @echo "Running Ruff..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix
lint: lint:
@echo "Running Ruff checks..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments @echo "Running Ruff checks..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments
format-payments:
@echo "Running Ruff format ONLY on handlers/payments..." && ruff format handlers/payments --config pyproject.toml
@echo "Running Ruff check ONLY on handlers/payments..." && ruff check handlers/payments --config pyproject.toml --fix
+1
View File
@@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
+1 -2
View File
@@ -10,10 +10,10 @@ from aiogram.types import BufferedInputFile, ErrorEvent
from aiogram.utils.markdown import hbold from aiogram.utils.markdown import hbold
from config import ADMIN_ID, API_TOKEN from config import ADMIN_ID, API_TOKEN
from database import async_session_maker
from filters.private import IsPrivateFilter from filters.private import IsPrivateFilter
from logger import logger from logger import logger
from utils.modules_loader import load_modules_from_folder from utils.modules_loader import load_modules_from_folder
from database import async_session_maker
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
@@ -129,4 +129,3 @@ async def errors_handler(event: ErrorEvent, bot: Bot) -> bool:
logger.warning(f"Не удалось отправить детали ошибки: {exception}") logger.warning(f"Не удалось отправить детали ошибки: {exception}")
except Exception as exception: except Exception as exception:
logger.error(f"Неожиданная ошибка в error handler: {exception}") logger.error(f"Неожиданная ошибка в error handler: {exception}")
+2 -4
View File
@@ -113,6 +113,7 @@ def restore_from_backup():
try: try:
mtime = os.path.getmtime(BACK_DIR) mtime = os.path.getmtime(BACK_DIR)
from datetime import datetime from datetime import datetime
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S") dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
console.print(f"[cyan]Обнаружен бэкап от: {dt}[/cyan]") console.print(f"[cyan]Обнаружен бэкап от: {dt}[/cyan]")
except Exception: except Exception:
@@ -129,10 +130,7 @@ def restore_from_backup():
install_rsync_if_needed() install_rsync_if_needed()
console.print("[yellow]Копирую файлы из бэкапа в проект...[/yellow]") console.print("[yellow]Копирую файлы из бэкапа в проект...[/yellow]")
rc = subprocess.run( rc = subprocess.run(f"rsync -a --delete {BACK_DIR}/ {PROJECT_DIR}/", shell=True).returncode
f"rsync -a --delete {BACK_DIR}/ {PROJECT_DIR}/",
shell=True
).returncode
if rc != 0: if rc != 0:
console.print("[red]❌ Ошибка rsync при восстановлении[/red]") console.print("[red]❌ Ошибка rsync при восстановлении[/red]")
return return
+1 -1
View File
@@ -20,4 +20,4 @@ async def get_hot_leads(session: AsyncSession):
) )
result = await session.execute(stmt) result = await session.execute(stmt)
return [row.tg_id for row in result] return [row.tg_id for row in result]
+2 -2
View File
@@ -45,9 +45,9 @@ async def store_key(
try: try:
await clear_hot_lead_notifications(session, tg_id) await clear_hot_lead_notifications(session, tg_id)
except Exception as e: except Exception:
pass pass
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при сохранении ключа: {e}") logger.error(f"❌ Ошибка при сохранении ключа: {e}")
await session.rollback() await session.rollback()
+11 -19
View File
@@ -3,18 +3,8 @@ import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import ( from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime, Float, ForeignKey, Integer, Numeric, String, Text
JSON, from sqlalchemy.dialects.postgresql import JSONB
BigInteger,
Boolean,
Column,
DateTime,
Float,
ForeignKey,
Integer,
String,
Text,
)
from sqlalchemy.orm import Mapped, declarative_base, mapped_column from sqlalchemy.orm import Mapped, declarative_base, mapped_column
@@ -43,14 +33,16 @@ class User(DictLikeMixin, Base):
is_bot = Column(Boolean, default=False) is_bot = Column(Boolean, default=False)
balance = Column(Float, default=0.0) balance = Column(Float, default=0.0)
trial = Column(Integer, default=0) trial = Column(Integer, default=0)
preferred_currency = Column(String(10), nullable=False, server_default="RUB", index=True)
source_code = Column( source_code = Column(
String, String,
ForeignKey( ForeignKey(
"tracking_sources.code", "tracking_sources.code",
ondelete="SET NULL", ondelete="SET NULL",
onupdate="CASCADE", onupdate="CASCADE",
), ),
nullable=True, ) nullable=True,
)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow)
@@ -114,6 +106,10 @@ class Payment(DictLikeMixin, Base):
payment_system = Column(String) payment_system = Column(String)
status = Column(String) status = Column(String)
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
original_amount = Column(Numeric(18, 8), nullable=True)
currency = Column(String(10), nullable=False, server_default="RUB")
payment_id = Column(String(128), nullable=True, index=True)
metadata_ = Column("metadata", JSONB, nullable=True)
class Coupon(DictLikeMixin, Base): class Coupon(DictLikeMixin, Base):
@@ -131,11 +127,7 @@ class Coupon(DictLikeMixin, Base):
class CouponUsage(DictLikeMixin, Base): class CouponUsage(DictLikeMixin, Base):
__tablename__ = "coupon_usages" __tablename__ = "coupon_usages"
coupon_id = Column( coupon_id = Column(Integer, ForeignKey("coupons.id", ondelete="CASCADE"), primary_key=True)
Integer,
ForeignKey("coupons.id", ondelete="CASCADE"),
primary_key=True
)
user_id = Column(BigInteger, primary_key=True) user_id = Column(BigInteger, primary_key=True)
used_at = Column(DateTime, default=datetime.utcnow) used_at = Column(DateTime, default=datetime.utcnow)
+13 -13
View File
@@ -70,32 +70,27 @@ async def check_hot_lead_discount(session: AsyncSession, tg_id: int) -> dict:
result = await session.execute( result = await session.execute(
select(Notification.notification_type, Notification.last_notification_time) select(Notification.notification_type, Notification.last_notification_time)
.where(Notification.tg_id == tg_id) .where(Notification.tg_id == tg_id)
.where(Notification.notification_type.in_(['hot_lead_step_2', 'hot_lead_step_3'])) .where(Notification.notification_type.in_(["hot_lead_step_2", "hot_lead_step_3"]))
.order_by(Notification.last_notification_time.desc()) .order_by(Notification.last_notification_time.desc())
.limit(1) .limit(1)
) )
row = result.first() row = result.first()
if not row: if not row:
return {"available": False} return {"available": False}
notification_type, last_time = row notification_type, last_time = row
expires_at = last_time + timedelta(hours=DISCOUNT_ACTIVE_HOURS) expires_at = last_time + timedelta(hours=DISCOUNT_ACTIVE_HOURS)
current_time = datetime.utcnow() current_time = datetime.utcnow()
if current_time > expires_at: if current_time > expires_at:
return {"available": False} return {"available": False}
tariff_group = "discounts" if notification_type == "hot_lead_step_2" else "discounts_max" tariff_group = "discounts" if notification_type == "hot_lead_step_2" else "discounts_max"
return { return {"available": True, "type": notification_type, "tariff_group": tariff_group, "expires_at": expires_at}
"available": True,
"type": notification_type,
"tariff_group": tariff_group,
"expires_at": expires_at
}
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка при проверке скидки горячего лида для {tg_id}: {e}") logger.error(f"❌ Ошибка при проверке скидки горячего лида для {tg_id}: {e}")
return {"available": False} return {"available": False}
@@ -106,7 +101,12 @@ async def clear_hot_lead_notifications(session: AsyncSession, tg_id: int):
await session.execute( await session.execute(
delete(Notification).where( delete(Notification).where(
Notification.tg_id == tg_id, Notification.tg_id == tg_id,
Notification.notification_type.in_(['hot_lead_step_1', 'hot_lead_step_2', 'hot_lead_step_3', 'hot_lead_step_2_expired']) Notification.notification_type.in_([
"hot_lead_step_1",
"hot_lead_step_2",
"hot_lead_step_3",
"hot_lead_step_2_expired",
]),
) )
) )
await session.commit() await session.commit()
+142 -21
View File
@@ -1,7 +1,8 @@
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional
from pytz import timezone from pytz import timezone
from sqlalchemy import insert, select from sqlalchemy import and_, insert, select, update
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,33 +13,153 @@ from logger import logger
MOSCOW_TZ = timezone("Europe/Moscow") MOSCOW_TZ = timezone("Europe/Moscow")
async def add_payment(session: AsyncSession, tg_id: int, amount: float, payment_system: str): async def add_payment(
session: AsyncSession,
tg_id: int,
amount: float,
payment_system: str,
*,
status: str = "success",
currency: str = "RUB",
payment_id: str | None = None,
metadata: dict | None = None,
original_amount: float | None = None,
) -> int:
try: try:
now_moscow = datetime.now(MOSCOW_TZ).replace(tzinfo=None) now_moscow = datetime.now(MOSCOW_TZ).replace(tzinfo=None)
stmt = insert(Payment).values( stmt = (
tg_id=tg_id, insert(Payment)
amount=amount, .values(
payment_system=payment_system, tg_id=tg_id,
status="success", amount=amount,
created_at=now_moscow, payment_system=payment_system,
status=status,
created_at=now_moscow,
currency=currency,
payment_id=payment_id,
metadata_=metadata,
original_amount=original_amount,
)
.returning(Payment.id)
) )
await session.execute(stmt) result = await session.execute(stmt)
internal_id = result.scalar_one()
await session.commit() await session.commit()
logger.info(f"✅ Успешно добавлен платёж: {tg_id}, {amount}₽ через {payment_system}") logger.info(
f"Добавлен платёж id={internal_id}: tg_id={tg_id}, amount={amount}, system={payment_system}, status={status}"
)
return internal_id
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"❌ Ошибка при добавлении платежа: {e}")
await session.rollback() await session.rollback()
logger.error(f"Ошибка при добавлении платежа: {e}")
raise raise
async def get_last_payments(session: AsyncSession, tg_id: int, limit: int = 3): async def get_last_payments(
session: AsyncSession,
tg_id: int,
limit: int = 3,
statuses: list[str] | None = None,
):
query = select(Payment).where(Payment.tg_id == tg_id)
if statuses:
query = query.where(Payment.status.in_(statuses))
query = query.order_by(Payment.created_at.desc()).limit(limit)
result = await session.execute(query)
payments = result.scalars().all()
return [
{
"id": p.id,
"tg_id": p.tg_id,
"amount": p.amount,
"currency": p.currency,
"status": p.status,
"payment_system": p.payment_system,
"payment_id": p.payment_id,
"created_at": p.created_at,
"metadata": p.metadata_,
"original_amount": p.original_amount,
}
for p in payments
]
async def get_payment_by_id(session: AsyncSession, internal_id: int) -> dict | None:
try: try:
result = await session.execute( result = await session.execute(select(Payment).where(Payment.id == internal_id).limit(1))
select(Payment).where(Payment.tg_id == tg_id).order_by(Payment.created_at.desc()).limit(limit) payment = result.scalar_one_or_none()
) if payment:
payments = result.scalars().all() logger.info(f"Найден платёж id={internal_id}")
logger.info(f"✅ Получены последние платежи пользователя {tg_id}, всего: {len(payments)}") return dict(payment.__dict__)
return [dict(p.__dict__) for p in payments] logger.info(f"Платёж id={internal_id} не найден")
return None
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"Ошибка при получении платежей пользователя {tg_id}: {e}") logger.error(f"Ошибка при поиске платежа id={internal_id}: {e}")
return [] return None
async def update_payment_status(
session: AsyncSession,
internal_id: int,
new_status: str,
*,
payment_id: str | None = None,
metadata_patch: dict | None = None,
) -> bool:
try:
result = await session.execute(select(Payment).where(Payment.id == internal_id).limit(1))
payment = result.scalar_one_or_none()
if not payment:
logger.info(f"Не удалось сменить статус: платёж id={internal_id} не найден")
return False
payment.status = new_status
if payment_id is not None:
payment.payment_id = payment_id
if metadata_patch:
base = payment.metadata_ or {}
base.update(metadata_patch)
payment.metadata_ = base
await session.commit()
logger.info(f"Статус платежа id={internal_id} изменён на {new_status}")
return True
except SQLAlchemyError as e:
await session.rollback()
logger.error(f"Ошибка при смене статуса платежа id={internal_id}: {e}")
return False
async def get_payment_by_payment_id(session: AsyncSession, pid: str) -> dict | None:
try:
result = await session.execute(select(Payment).where(Payment.payment_id == pid).limit(1))
payment = result.scalar_one_or_none()
if payment:
logger.info(f"Найден платёж payment_id={pid}")
return dict(payment.__dict__)
logger.info(f"Платёж payment_id={pid} не найден")
return None
except SQLAlchemyError as e:
logger.error(f"Ошибка при поиске платежа payment_id={pid}: {e}")
return None
async def cancel_expired_pending_payments(session: AsyncSession) -> int:
cutoff = datetime.now(MOSCOW_TZ).replace(tzinfo=None) - timedelta(minutes=60)
stmt = (
update(Payment)
.where(
and_(
Payment.status.in_(("pending", "issued", "processing", "awaiting_choice")),
Payment.created_at < cutoff,
)
)
.values(status="cancelled")
.returning(Payment.id)
)
res = await session.execute(stmt)
await session.commit()
return len(res.fetchall())
+13 -11
View File
@@ -34,15 +34,9 @@ async def count_active_keys(session: AsyncSession) -> int:
async def count_trial_keys(session: AsyncSession) -> int: async def count_trial_keys(session: AsyncSession) -> int:
trial_tariffs_subquery = ( trial_tariffs_subquery = select(Tariff.id).where(Tariff.group_code == "trial")
select(Tariff.id).where(Tariff.group_code == "trial")
) return await session.scalar(select(func.count()).select_from(Key).where(Key.tariff_id.in_(trial_tariffs_subquery)))
return await session.scalar(
select(func.count())
.select_from(Key)
.where(Key.tariff_id.in_(trial_tariffs_subquery))
)
async def get_tariff_distribution( async def get_tariff_distribution(
@@ -93,7 +87,11 @@ async def count_total_referrals(session: AsyncSession) -> int:
async def sum_payments_since(session: AsyncSession, since: date) -> float: async def sum_payments_since(session: AsyncSession, since: date) -> float:
result = await session.scalar( result = await session.scalar(
select(func.coalesce(func.sum(Payment.amount), 0)).where( select(func.coalesce(func.sum(Payment.amount), 0)).where(
and_(Payment.created_at >= since, Payment.payment_system.notin_(["referral", "coupon", "cashback"])) and_(
Payment.created_at >= since,
Payment.status == "success",
Payment.payment_system.notin_(["referral", "coupon", "cashback"]),
)
) )
) )
return round(float(result), 2) return round(float(result), 2)
@@ -105,6 +103,7 @@ async def sum_payments_between(session: AsyncSession, start: date, end: date) ->
and_( and_(
Payment.created_at >= start, Payment.created_at >= start,
Payment.created_at < end, Payment.created_at < end,
Payment.status == "success",
Payment.payment_system.notin_(["referral", "coupon", "cashback"]), Payment.payment_system.notin_(["referral", "coupon", "cashback"]),
) )
) )
@@ -115,7 +114,10 @@ async def sum_payments_between(session: AsyncSession, start: date, end: date) ->
async def sum_total_payments(session: AsyncSession) -> float: async def sum_total_payments(session: AsyncSession) -> float:
result = await session.scalar( result = await session.scalar(
select(func.coalesce(func.sum(Payment.amount), 0)).where( select(func.coalesce(func.sum(Payment.amount), 0)).where(
Payment.payment_system.notin_(["referral", "coupon", "cashback"]) and_(
Payment.status == "success",
Payment.payment_system.notin_(["referral", "coupon", "cashback"]),
)
) )
) )
return round(float(result), 2) return round(float(result), 2)
+33 -56
View File
@@ -1,6 +1,6 @@
import hashlib import hashlib
from collections import defaultdict
from collections import defaultdict
from datetime import datetime from datetime import datetime
from sqlalchemy import delete, func, insert, select, update from sqlalchemy import delete, func, insert, select, update
@@ -35,15 +35,15 @@ async def find_subgroup_by_hash(session: AsyncSession, subgroup_hash: str, group
return None return None
async def get_tariffs(session: AsyncSession, tariff_id: int = None, group_code: str = None, with_subgroup_weights: bool = False): async def get_tariffs(
session: AsyncSession, tariff_id: int = None, group_code: str = None, with_subgroup_weights: bool = False
):
try: try:
if tariff_id: if tariff_id:
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
elif group_code: elif group_code:
result = await session.execute( result = await session.execute(
select(Tariff) select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.sort_order, Tariff.id)
.where(Tariff.group_code == group_code)
.order_by(Tariff.sort_order, Tariff.id)
) )
else: else:
result = await session.execute(select(Tariff).order_by(Tariff.sort_order, Tariff.id)) result = await session.execute(select(Tariff).order_by(Tariff.sort_order, Tariff.id))
@@ -55,26 +55,21 @@ async def get_tariffs(session: AsyncSession, tariff_id: int = None, group_code:
if tariffs_without_order: if tariffs_without_order:
for tariff in tariffs_without_order: for tariff in tariffs_without_order:
tariff["sort_order"] = 1 tariff["sort_order"] = 1
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff["id"]).values(sort_order=1))
update(Tariff).where(Tariff.id == tariff["id"]).values(sort_order=1)
)
await session.commit() await session.commit()
grouped = defaultdict(list) grouped = defaultdict(list)
for t in tariffs: for t in tariffs:
grouped[t.get("subgroup_title")].append(t) grouped[t.get("subgroup_title")].append(t)
subgroup_weights = {} subgroup_weights = {}
for subgroup, tariffs_list in grouped.items(): for subgroup, tariffs_list in grouped.items():
if subgroup: if subgroup:
total_weight = sum(t.get("sort_order", 1) for t in tariffs_list) total_weight = sum(t.get("sort_order", 1) for t in tariffs_list)
subgroup_weights[subgroup] = total_weight subgroup_weights[subgroup] = total_weight
return { return {"tariffs": tariffs, "subgroup_weights": subgroup_weights}
'tariffs': tariffs,
'subgroup_weights': subgroup_weights
}
return tariffs return tariffs
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[TARIFF] Ошибка при получении тарифов: {e}") logger.error(f"[TARIFF] Ошибка при получении тарифов: {e}")
@@ -128,15 +123,13 @@ async def create_tariff(session: AsyncSession, data: dict):
group_code = data.get("group_code") group_code = data.get("group_code")
if group_code: if group_code:
result = await session.execute( result = await session.execute(
select(func.max(Tariff.sort_order)) select(func.max(Tariff.sort_order)).where(
.where(Tariff.group_code == group_code, Tariff.sort_order.isnot(None)) Tariff.group_code == group_code, Tariff.sort_order.isnot(None)
)
) )
max_order = result.scalar() or 0 max_order = result.scalar() or 0
else: else:
result = await session.execute( result = await session.execute(select(func.max(Tariff.sort_order)).where(Tariff.sort_order.isnot(None)))
select(func.max(Tariff.sort_order))
.where(Tariff.sort_order.isnot(None))
)
max_order = result.scalar() or 0 max_order = result.scalar() or 0
data["sort_order"] = max_order + 1 data["sort_order"] = max_order + 1
@@ -191,18 +184,14 @@ async def check_tariff_exists(session: AsyncSession, tariff_id: int):
async def get_tariff_sort_order(session: AsyncSession, tariff_id: int) -> int: async def get_tariff_sort_order(session: AsyncSession, tariff_id: int) -> int:
try: try:
result = await session.execute( result = await session.execute(select(Tariff.sort_order).where(Tariff.id == tariff_id))
select(Tariff.sort_order).where(Tariff.id == tariff_id)
)
sort_order = result.scalar_one_or_none() sort_order = result.scalar_one_or_none()
if sort_order is None: if sort_order is None:
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff_id).values(sort_order=1))
update(Tariff).where(Tariff.id == tariff_id).values(sort_order=1)
)
await session.commit() await session.commit()
return 1 return 1
return sort_order return sort_order
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[TARIFF] Ошибка при получении sort_order для тарифа {tariff_id}: {e}") logger.error(f"[TARIFF] Ошибка при получении sort_order для тарифа {tariff_id}: {e}")
@@ -213,10 +202,8 @@ async def move_tariff_up(session: AsyncSession, tariff_id: int) -> bool:
try: try:
current_order = await get_tariff_sort_order(session, tariff_id) current_order = await get_tariff_sort_order(session, tariff_id)
new_order = max(1, current_order - 1) new_order = max(1, current_order - 1)
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order))
update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order)
)
await session.commit() await session.commit()
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
@@ -229,10 +216,8 @@ async def move_tariff_down(session: AsyncSession, tariff_id: int) -> bool:
try: try:
current_order = await get_tariff_sort_order(session, tariff_id) current_order = await get_tariff_sort_order(session, tariff_id)
new_order = current_order + 1 new_order = current_order + 1
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order))
update(Tariff).where(Tariff.id == tariff_id).values(sort_order=new_order)
)
await session.commit() await session.commit()
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
@@ -243,20 +228,16 @@ async def move_tariff_down(session: AsyncSession, tariff_id: int) -> bool:
async def initialize_tariff_sort_orders(session: AsyncSession, group_code: str) -> bool: async def initialize_tariff_sort_orders(session: AsyncSession, group_code: str) -> bool:
try: try:
result = await session.execute( result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id))
select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)
)
tariffs = result.scalars().all() tariffs = result.scalars().all()
if not tariffs: if not tariffs:
return True return True
for i, tariff in enumerate(tariffs): for i, tariff in enumerate(tariffs):
new_sort_order = 1 + i new_sort_order = 1 + i
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff.id).values(sort_order=new_sort_order))
update(Tariff).where(Tariff.id == tariff.id).values(sort_order=new_sort_order)
)
await session.commit() await session.commit()
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
@@ -267,23 +248,19 @@ async def initialize_tariff_sort_orders(session: AsyncSession, group_code: str)
async def initialize_all_tariff_weights(session: AsyncSession) -> bool: async def initialize_all_tariff_weights(session: AsyncSession) -> bool:
try: try:
result = await session.execute( result = await session.execute(select(Tariff).where(Tariff.sort_order.is_(None)))
select(Tariff).where(Tariff.sort_order.is_(None))
)
tariffs_without_weight = result.scalars().all() tariffs_without_weight = result.scalars().all()
if not tariffs_without_weight: if not tariffs_without_weight:
return True return True
for tariff in tariffs_without_weight: for tariff in tariffs_without_weight:
await session.execute( await session.execute(update(Tariff).where(Tariff.id == tariff.id).values(sort_order=1))
update(Tariff).where(Tariff.id == tariff.id).values(sort_order=1)
)
await session.commit() await session.commit()
return True return True
except SQLAlchemyError as e: except SQLAlchemyError as e:
logger.error(f"[TARIFF] Ошибка при инициализации весов тарифов: {e}") logger.error(f"[TARIFF] Ошибка при инициализации весов тарифов: {e}")
await session.rollback() await session.rollback()
return False return False
+5 -9
View File
@@ -1,4 +1,4 @@
from sqlalchemy import func, insert, not_, select, and_ from sqlalchemy import and_, func, insert, not_, select
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,6 +8,7 @@ from logger import logger
EXCLUDED_PAYMENT_MARKERS = ["coupon", "referral", "cashback"] EXCLUDED_PAYMENT_MARKERS = ["coupon", "referral", "cashback"]
async def create_tracking_source(session: AsyncSession, name: str, code: str, type_: str, created_by: int): async def create_tracking_source(session: AsyncSession, name: str, code: str, type_: str, created_by: int):
try: try:
stmt = insert(TrackingSource).values( stmt = insert(TrackingSource).values(
@@ -76,14 +77,13 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict |
return dt.strftime("%Y-%m") return dt.strftime("%Y-%m")
src_row = await session.execute( src_row = await session.execute(
select(TrackingSource.name, TrackingSource.code, TrackingSource.created_at) select(TrackingSource.name, TrackingSource.code, TrackingSource.created_at).where(TrackingSource.code == code)
.where(TrackingSource.code == code)
) )
src = src_row.first() src = src_row.first()
if not src: if not src:
return None return None
src_name, src_code, created_at = src _src_name, _src_code, created_at = src
reg_subq = ( reg_subq = (
select(func.count(func.distinct(User.tg_id))) select(func.count(func.distinct(User.tg_id)))
@@ -212,11 +212,7 @@ async def get_tracking_source_stats(session: AsyncSession, code: str) -> dict |
month_expr_trials, month_expr_trials,
func.count(func.distinct(User.tg_id)).label("cnt"), func.count(func.distinct(User.tg_id)).label("cnt"),
) )
.where( .where((User.source_code == code) & (User.trial == 1) & (User.created_at >= created_at))
(User.source_code == code)
& (User.trial == 1)
& (User.created_at >= created_at)
)
.group_by(month_expr_trials) .group_by(month_expr_trials)
.order_by(month_expr_trials) .order_by(month_expr_trials)
) )
-2
View File
@@ -9,7 +9,6 @@ from .donate import router as donate_router
from .instructions import router as instructions_router from .instructions import router as instructions_router
from .keys import router as keys_router from .keys import router as keys_router
from .notifications import router as notifications_router from .notifications import router as notifications_router
from .pay import router as pay_router
from .payments import router as payments_router from .payments import router as payments_router
from .profile import router as profile_router from .profile import router as profile_router
from .refferal import router as refferal_router from .refferal import router as refferal_router
@@ -22,7 +21,6 @@ router.include_routers(
start_router, start_router,
captcha_router, captcha_router,
profile_router, profile_router,
pay_router,
donate_router, donate_router,
coupons_router, coupons_router,
notifications_router, notifications_router,
+15 -18
View File
@@ -1,9 +1,10 @@
from collections import defaultdict
from aiogram import Bot, F, Router, types from aiogram import Bot, F, Router, types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from collections import defaultdict
from sqlalchemy import delete, func, select from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -34,8 +35,8 @@ async def admin_gift_menu(callback: CallbackQuery):
@router.callback_query(F.data == "admin_gift_create") @router.callback_query(F.data == "admin_gift_create")
async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession): async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession):
tariffs_data = await get_tariffs(session, group_code="gifts", with_subgroup_weights=True) tariffs_data = await get_tariffs(session, group_code="gifts", with_subgroup_weights=True)
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] tariffs = [t for t in tariffs_data["tariffs"] if t.get("is_active")]
subgroup_weights = tariffs_data['subgroup_weights'] subgroup_weights = tariffs_data["subgroup_weights"]
if not tariffs: if not tariffs:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
@@ -45,28 +46,24 @@ async def admin_create_gift_step1(callback: CallbackQuery, session: AsyncSession
grouped_tariffs = defaultdict(list) grouped_tariffs = defaultdict(list)
for t in tariffs: for t in tariffs:
grouped_tariffs[t.get('subgroup_title')].append(t) grouped_tariffs[t.get("subgroup_title")].append(t)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for t in grouped_tariffs.get(None, []): for t in grouped_tariffs.get(None, []):
if t.get('duration_days') % 30 == 0: if t.get("duration_days") % 30 == 0:
duration_text = format_months(t.get('duration_days') // 30) duration_text = format_months(t.get("duration_days") // 30)
else: else:
duration_text = format_days(t.get('duration_days')) duration_text = format_days(t.get("duration_days"))
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
text=f"{t.get('name')} {duration_text}", text=f"{t.get('name')} {duration_text}", callback_data=f"admin_gift_select|{t.get('id')}"
callback_data=f"admin_gift_select|{t.get('id')}"
) )
) )
sorted_subgroups = sorted( sorted_subgroups = sorted([k for k in grouped_tariffs if k], key=lambda x: (subgroup_weights.get(x, 999999), x))
[k for k in grouped_tariffs if k],
key=lambda x: (subgroup_weights.get(x, 999999), x)
)
for subgroup in sorted_subgroups: for subgroup in sorted_subgroups:
subgroup_hash = create_subgroup_hash(subgroup, "gifts") subgroup_hash = create_subgroup_hash(subgroup, "gifts")
builder.row( builder.row(
@@ -92,17 +89,17 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session:
return return
tariffs = await get_tariffs(session, group_code="gifts") tariffs = await get_tariffs(session, group_code="gifts")
filtered = [t for t in tariffs if t.get('subgroup_title') == subgroup and t.get('is_active')] filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup and t.get("is_active")]
if not filtered: if not filtered:
await callback.message.edit_text("❌ В этой подгруппе пока нет тарифов.") await callback.message.edit_text("❌ В этой подгруппе пока нет тарифов.")
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for t in filtered: for t in filtered:
if t.get('duration_days') % 30 == 0: if t.get("duration_days") % 30 == 0:
duration_text = format_months(t.get('duration_days') // 30) duration_text = format_months(t.get("duration_days") // 30)
else: else:
duration_text = format_days(t.get('duration_days')) duration_text = format_days(t.get("duration_days"))
builder.row( builder.row(
types.InlineKeyboardButton( types.InlineKeyboardButton(
@@ -1,11 +1,11 @@
import hashlib import hashlib
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import time import time
import traceback import traceback
import re
from asyncio import sleep from asyncio import sleep
from datetime import datetime from datetime import datetime
+12 -10
View File
@@ -12,7 +12,7 @@ from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMar
from sqlalchemy import distinct, func, select from sqlalchemy import distinct, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database.models import Key, Payment, Server, Tariff, User, BlockedUser, ManualBan from database.models import BlockedUser, Key, ManualBan, Payment, Server, Tariff, User
from filters.admin import IsAdminFilter from filters.admin import IsAdminFilter
from logger import logger from logger import logger
@@ -234,17 +234,15 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext,
except Exception as e: except Exception as e:
logger.error(f"Ошибка восстановления клавиатуры: {e}") logger.error(f"Ошибка восстановления клавиатуры: {e}")
banned_tg_ids = ( banned_tg_ids = select(BlockedUser.tg_id).union_all(
select(BlockedUser.tg_id).union_all( select(ManualBan.tg_id).where((ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()))
select(ManualBan.tg_id).where(
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow())
)
)
) )
query = None query = None
if send_to == "subscribed": if send_to == "subscribed":
query = select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms).where(~User.tg_id.in_(banned_tg_ids)) query = (
select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms).where(~User.tg_id.in_(banned_tg_ids))
)
elif send_to == "unsubscribed": elif send_to == "unsubscribed":
subquery = ( subquery = (
select(User.tg_id) select(User.tg_id)
@@ -261,7 +259,11 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext,
query = select(distinct(subquery.c.tg_id)).where(~subquery.c.tg_id.in_(banned_tg_ids)) query = select(distinct(subquery.c.tg_id)).where(~subquery.c.tg_id.in_(banned_tg_ids))
elif send_to == "untrial": elif send_to == "untrial":
subquery = select(Key.tg_id) subquery = select(Key.tg_id)
query = select(distinct(User.tg_id)).where(~User.tg_id.in_(subquery) & User.trial.in_([0, -1])).where(~User.tg_id.in_(banned_tg_ids)) query = (
select(distinct(User.tg_id))
.where(~User.tg_id.in_(subquery) & User.trial.in_([0, -1]))
.where(~User.tg_id.in_(banned_tg_ids))
)
elif send_to == "cluster": elif send_to == "cluster":
query = ( query = (
select(distinct(User.tg_id)) select(distinct(User.tg_id))
@@ -281,7 +283,7 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext,
) )
elif send_to == "trial": elif send_to == "trial":
trial_tariff_subquery = select(Tariff.id).where(Tariff.group_code == "trial") trial_tariff_subquery = select(Tariff.id).where(Tariff.group_code == "trial")
query = ( query = (
select(distinct(Key.tg_id)) select(distinct(Key.tg_id))
.where(Key.tariff_id.in_(trial_tariff_subquery)) .where(Key.tariff_id.in_(trial_tariff_subquery))
+2 -1
View File
@@ -29,6 +29,7 @@ from database import (
sum_total_payments, sum_total_payments,
) )
from filters.admin import IsAdminFilter from filters.admin import IsAdminFilter
from hooks.hooks import run_hooks
from logger import logger from logger import logger
from utils.csv_export import ( from utils.csv_export import (
export_hot_leads_csv, export_hot_leads_csv,
@@ -39,7 +40,7 @@ from utils.csv_export import (
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import build_stats_kb from .keyboard import build_stats_kb
from hooks.hooks import run_hooks
router = Router() router = Router()
+31 -15
View File
@@ -57,7 +57,7 @@ def build_tariff_arrangement_groups_kb(groups: list[str]) -> InlineKeyboardMarku
if len(row) == 2 or i == len(groups) - 1: if len(row) == 2 or i == len(groups) - 1:
builder.row(*row) builder.row(*row)
row = [] row = []
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="⬅️ Назад", text="⬅️ Назад",
@@ -80,9 +80,17 @@ def build_tariffs_arrangement_kb(group_code: str, tariffs: list) -> InlineKeyboa
if grouped_tariffs.get(None): if grouped_tariffs.get(None):
for t in grouped_tariffs[None]: for t in grouped_tariffs[None]:
builder.row( builder.row(
InlineKeyboardButton(text="⬆️", callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack()), InlineKeyboardButton(
InlineKeyboardButton(text=f" {t.get('name')} ", callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack()), text="⬆️",
InlineKeyboardButton(text="⬇️", callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack()) callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack(),
),
InlineKeyboardButton(
text=f" {t.get('name')} ", callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack()
),
InlineKeyboardButton(
text="⬇️",
callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack(),
),
) )
for subgroup, tariffs_list in grouped_tariffs.items(): for subgroup, tariffs_list in grouped_tariffs.items():
@@ -92,11 +100,20 @@ def build_tariffs_arrangement_kb(group_code: str, tariffs: list) -> InlineKeyboa
) )
for t in tariffs_list: for t in tariffs_list:
builder.row( builder.row(
InlineKeyboardButton(text="⬆️", callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack()), InlineKeyboardButton(
InlineKeyboardButton(text=f" {t.get('name')} ", callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack()), text="⬆️",
InlineKeyboardButton(text="⬇️", callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack()) callback_data=AdminTariffCallback(action=f"quick_move_up|{t.get('id')}|{group_code}").pack(),
),
InlineKeyboardButton(
text=f" {t.get('name')} ",
callback_data=AdminTariffCallback(action=f"view|{t.get('id')}").pack(),
),
InlineKeyboardButton(
text="⬇️",
callback_data=AdminTariffCallback(action=f"quick_move_down|{t.get('id')}|{group_code}").pack(),
),
) )
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text="⬅️ Назад", text="⬅️ Назад",
@@ -109,7 +126,7 @@ def build_tariffs_arrangement_kb(group_code: str, tariffs: list) -> InlineKeyboa
callback_data=AdminPanelCallback(action="admin").pack(), callback_data=AdminPanelCallback(action="admin").pack(),
) )
) )
return builder.as_markup() return builder.as_markup()
@@ -150,16 +167,13 @@ def build_tariff_list_kb(tariffs: list[dict]) -> InlineKeyboardMarkup:
grouped[subgroup].append(t) grouped[subgroup].append(t)
sorted_subgroups = sorted( sorted_subgroups = sorted(
[k for k in grouped if k], [k for k in grouped if k], key=lambda x: (sum(t.get("sort_order", 1) for t in grouped[x]), x)
key=lambda x: (sum(t.get("sort_order", 1) for t in grouped[x]), x)
) )
for subgroup_title in sorted_subgroups: for subgroup_title in sorted_subgroups:
subgroup_hash = create_subgroup_hash(subgroup_title, group_code) subgroup_hash = create_subgroup_hash(subgroup_title, group_code)
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(text=f"{subgroup_title}", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")
text=f"{subgroup_title}", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"
)
) )
for t in grouped.get(None, []): for t in grouped.get(None, []):
@@ -216,7 +230,9 @@ def build_single_tariff_kb(tariff_id: int, group_code: str = None) -> InlineKeyb
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="⬅️ Назад", text="⬅️ Назад",
callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() if group_code else AdminTariffCallback(action="list").pack(), callback_data=AdminTariffCallback(action=f"group|{group_code}").pack()
if group_code
else AdminTariffCallback(action="list").pack(),
) )
], ],
] ]
+43 -26
View File
@@ -1,7 +1,8 @@
import re import re
from collections import defaultdict
from collections import defaultdict
from datetime import datetime from datetime import datetime
import pytz import pytz
from aiogram import F, Router from aiogram import F, Router
@@ -20,11 +21,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from database import create_tariff from database import create_tariff
from database.models import Gift, Key, Server, Tariff from database.models import Gift, Key, Server, Tariff
from database.tariffs import ( from database.tariffs import (
create_subgroup_hash, create_subgroup_hash,
find_subgroup_by_hash, find_subgroup_by_hash,
get_tariffs, get_tariffs,
move_tariff_down as db_move_tariff_down,
move_tariff_up as db_move_tariff_up, move_tariff_up as db_move_tariff_up,
move_tariff_down as db_move_tariff_down
) )
from filters.admin import IsAdminFilter from filters.admin import IsAdminFilter
@@ -34,10 +35,10 @@ from .keyboard import (
build_cancel_kb, build_cancel_kb,
build_edit_tariff_fields_kb, build_edit_tariff_fields_kb,
build_single_tariff_kb, build_single_tariff_kb,
build_tariff_arrangement_groups_kb,
build_tariff_groups_kb, build_tariff_groups_kb,
build_tariff_list_kb, build_tariff_list_kb,
build_tariff_menu_kb, build_tariff_menu_kb,
build_tariff_arrangement_groups_kb,
build_tariffs_arrangement_kb, build_tariffs_arrangement_kb,
) )
@@ -348,7 +349,7 @@ async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTar
if not tariffs: if not tariffs:
await callback.message.edit_text("❌ В этой группе пока нет тарифов.") await callback.message.edit_text("❌ В этой группе пока нет тарифов.")
return return
tariff_dicts = [tariff_to_dict(t) for t in tariffs] tariff_dicts = [tariff_to_dict(t) for t in tariffs]
await callback.message.edit_text( await callback.message.edit_text(
@@ -362,8 +363,8 @@ async def show_tariffs_arrangement(callback: CallbackQuery, callback_data: Admin
group_code = callback_data.action.split("|")[1] group_code = callback_data.action.split("|")[1]
tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True)
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] tariffs = [t for t in tariffs_data["tariffs"] if t.get("is_active")]
subgroup_weights = tariffs_data['subgroup_weights'] subgroup_weights = tariffs_data["subgroup_weights"]
if not tariffs: if not tariffs:
await callback.message.edit_text("❌ В этой группе пока нет активных тарифов.") await callback.message.edit_text("❌ В этой группе пока нет активных тарифов.")
@@ -373,21 +374,18 @@ async def show_tariffs_arrangement(callback: CallbackQuery, callback_data: Admin
for t in tariffs: for t in tariffs:
grouped_tariffs[t.get("subgroup_title")].append(t) grouped_tariffs[t.get("subgroup_title")].append(t)
sorted_subgroups = sorted( sorted_subgroups = sorted([k for k in grouped_tariffs if k], key=lambda x: (subgroup_weights.get(x, 999999), x))
[k for k in grouped_tariffs if k],
key=lambda x: (subgroup_weights.get(x, 999999), x)
)
moscow_tz = pytz.timezone("Europe/Moscow") moscow_tz = pytz.timezone("Europe/Moscow")
now = datetime.now(moscow_tz) now = datetime.now(moscow_tz)
current_time = now.strftime("%d.%m.%y %H:%M:%S МСК") current_time = now.strftime("%d.%m.%y %H:%M:%S МСК")
text = f"🔢 <b>Итоговая сортировка тарифов в группе: {group_code}</b>\n\n" text = f"🔢 <b>Итоговая сортировка тарифов в группе: {group_code}</b>\n\n"
if grouped_tariffs.get(None): if grouped_tariffs.get(None):
text += "<b>📋 Основные тарифы:</b>\n" text += "<b>📋 Основные тарифы:</b>\n"
for t in grouped_tariffs[None]: for t in grouped_tariffs[None]:
sort_order = t.get('sort_order', 1) sort_order = t.get("sort_order", 1)
text += f"{t.get('name')} <code>[позиция: {sort_order}]</code>\n" text += f"{t.get('name')} <code>[позиция: {sort_order}]</code>\n"
text += "\n" text += "\n"
@@ -397,7 +395,7 @@ async def show_tariffs_arrangement(callback: CallbackQuery, callback_data: Admin
subgroup_weight = subgroup_weights.get(subgroup, 999999) subgroup_weight = subgroup_weights.get(subgroup, 999999)
text += f"• <b>{subgroup}</b> <code>[вес группы: {subgroup_weight}]</code>\n" text += f"• <b>{subgroup}</b> <code>[вес группы: {subgroup_weight}]</code>\n"
for t in grouped_tariffs[subgroup]: for t in grouped_tariffs[subgroup]:
sort_order = t.get('sort_order', 1) sort_order = t.get("sort_order", 1)
text += f"{t.get('name')} <code>[позиция: {sort_order}]</code>\n" text += f"{t.get('name')} <code>[позиция: {sort_order}]</code>\n"
text += "\n" text += "\n"
@@ -656,7 +654,7 @@ async def start_tariff_creation_existing_group(
def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]: def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]:
traffic_text = f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" traffic_text = f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит"
device_text = f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" device_text = f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит"
sort_order = getattr(tariff, 'sort_order', 1) sort_order = getattr(tariff, "sort_order", 1)
text = ( text = (
f"<b>📄 Тариф: {tariff.name}</b>\n\n" f"<b>📄 Тариф: {tariff.name}</b>\n\n"
@@ -732,7 +730,9 @@ async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMCo
for tariff in tariffs: for tariff in tariffs:
is_selected = tariff.get("id") in selected is_selected = tariff.get("id") in selected
prefix = "" if is_selected else "" prefix = "" if is_selected else ""
builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.get('name')}", callback_data=f"sub_select|{tariff.get('id')}")) builder.row(
InlineKeyboardButton(text=f"{prefix}{tariff.get('name')}", callback_data=f"sub_select|{tariff.get('id')}")
)
builder.row( builder.row(
InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"),
@@ -1018,7 +1018,11 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext
return return
all_tariffs_to_show = await get_tariffs(session, group_code=group_code) all_tariffs_to_show = await get_tariffs(session, group_code=group_code)
all_tariffs_to_show = [t for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""] all_tariffs_to_show = [
t
for t in all_tariffs_to_show
if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""
]
subgroup_tariff_ids = {t.get("id") for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title} subgroup_tariff_ids = {t.get("id") for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title}
@@ -1045,7 +1049,11 @@ async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext
for tariff in all_tariffs_to_show: for tariff in all_tariffs_to_show:
is_in_subgroup = tariff.get("id") in subgroup_tariff_ids is_in_subgroup = tariff.get("id") in subgroup_tariff_ids
prefix = "" if is_in_subgroup else "" prefix = "" if is_in_subgroup else ""
builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}")) builder.row(
InlineKeyboardButton(
text=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}"
)
)
builder.row( builder.row(
InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"),
@@ -1078,13 +1086,21 @@ async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMCont
subgroup_hash = data["subgroup_hash"] subgroup_hash = data["subgroup_hash"]
all_tariffs_to_show = await get_tariffs(session, group_code=group_code) all_tariffs_to_show = await get_tariffs(session, group_code=group_code)
all_tariffs_to_show = [t for t in all_tariffs_to_show if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""] all_tariffs_to_show = [
t
for t in all_tariffs_to_show
if t.get("subgroup_title") == subgroup_title or not t.get("subgroup_title") or t.get("subgroup_title") == ""
]
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for tariff in all_tariffs_to_show: for tariff in all_tariffs_to_show:
is_selected = tariff.get("id") in selected_ids is_selected = tariff.get("id") in selected_ids
prefix = "" if is_selected else "" prefix = "" if is_selected else ""
builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}")) builder.row(
InlineKeyboardButton(
text=f"{prefix}{tariff.get('name')}", callback_data=f"edit_sub_toggle|{tariff.get('id')}"
)
)
builder.row( builder.row(
InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"),
@@ -1183,14 +1199,14 @@ async def move_tariff_up(callback: CallbackQuery, callback_data: AdminTariffCall
tariff_id = int(callback_data.action.split("|")[1]) tariff_id = int(callback_data.action.split("|")[1])
success = await db_move_tariff_up(session, tariff_id) success = await db_move_tariff_up(session, tariff_id)
if not success: if not success:
await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True)
return return
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none() tariff = result.scalar_one_or_none()
if not tariff: if not tariff:
await callback.answer("❌ Тариф не найден", show_alert=True) await callback.answer("❌ Тариф не найден", show_alert=True)
return return
@@ -1205,14 +1221,14 @@ async def move_tariff_down(callback: CallbackQuery, callback_data: AdminTariffCa
tariff_id = int(callback_data.action.split("|")[1]) tariff_id = int(callback_data.action.split("|")[1])
success = await db_move_tariff_down(session, tariff_id) success = await db_move_tariff_down(session, tariff_id)
if not success: if not success:
await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True)
return return
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none() tariff = result.scalar_one_or_none()
if not tariff: if not tariff:
await callback.answer("❌ Тариф не найден", show_alert=True) await callback.answer("❌ Тариф не найден", show_alert=True)
return return
@@ -1229,7 +1245,7 @@ async def quick_move_tariff_up(callback: CallbackQuery, callback_data: AdminTari
group_code = parts[2] group_code = parts[2]
success = await db_move_tariff_up(session, tariff_id) success = await db_move_tariff_up(session, tariff_id)
if not success: if not success:
await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True)
return return
@@ -1238,6 +1254,7 @@ async def quick_move_tariff_up(callback: CallbackQuery, callback_data: AdminTari
new_callback_data = AdminTariffCallback(action=f"arrange_group|{group_code}") new_callback_data = AdminTariffCallback(action=f"arrange_group|{group_code}")
await show_tariffs_arrangement(callback, new_callback_data, session) await show_tariffs_arrangement(callback, new_callback_data, session)
@router.callback_query(AdminTariffCallback.filter(F.action.startswith("quick_move_down|")), IsAdminFilter()) @router.callback_query(AdminTariffCallback.filter(F.action.startswith("quick_move_down|")), IsAdminFilter())
async def quick_move_tariff_down(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): async def quick_move_tariff_down(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession):
parts = callback_data.action.split("|") parts = callback_data.action.split("|")
@@ -1245,7 +1262,7 @@ async def quick_move_tariff_down(callback: CallbackQuery, callback_data: AdminTa
group_code = parts[2] group_code = parts[2]
success = await db_move_tariff_down(session, tariff_id) success = await db_move_tariff_down(session, tariff_id)
if not success: if not success:
await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True) await callback.answer("❌ Ошибка при перемещении тарифа", show_alert=True)
return return
+21 -38
View File
@@ -10,8 +10,8 @@ from aiogram import F, Router, types
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from aiogram.utils.formatting import Text, Bold, BlockQuote
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from aiogram.utils.formatting import BlockQuote, Bold, Text
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import delete, func, or_, select, update from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.dialects.postgresql import insert as pg_insert
@@ -549,39 +549,35 @@ async def handle_key_edit(
update: bool = False, update: bool = False,
): ):
email = callback_data.data email = callback_data.data
key_details = await get_key_details(session, email) result = await session.execute(select(Key).where(Key.email == email))
key_obj: Key | None = result.scalar_one_or_none()
if not key_details: if not key_obj:
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="🚫 Информация о ключе не найдена.", text="🚫 Информация о ключе не найдена.",
reply_markup=build_editor_kb(callback_data.tg_id), reply_markup=build_editor_kb(callback_data.tg_id),
) )
return return
key_value = key_details.get("key") or key_details.get("remnawave_link") or "" key_value = key_obj.key or key_obj.remnawave_link or ""
alias = key_details.get("alias") alias_part = f" (<i>{key_obj.alias}</i>)" if key_obj.alias else ""
created_at_raw = key_details.get("created_at") if key_obj.created_at:
if created_at_raw: created_at_dt = datetime.fromtimestamp(int(key_obj.created_at) / 1000) + timedelta(hours=3)
created_at_dt = datetime.fromtimestamp(int(created_at_raw) / 1000) + timedelta(hours=3)
created_at = created_at_dt.strftime("%d %B %Y года %H:%M") created_at = created_at_dt.strftime("%d %B %Y года %H:%M")
else: else:
created_at = "" created_at = ""
if key_obj.expiry_time:
expiry_time_raw = key_details.get("expiry_time") expiry_dt = datetime.fromtimestamp(int(key_obj.expiry_time) / 1000)
if expiry_time_raw:
expiry_dt = datetime.fromtimestamp(int(expiry_time_raw) / 1000)
expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M") expiry_date = expiry_dt.strftime("%d %B %Y года %H:%M")
else: else:
expiry_date = "" expiry_date = ""
tariff_name = "" tariff_name = ""
subgroup_title = "" subgroup_title = ""
if key_details.get("tariff_id"): if key_obj.tariff_id:
result = await session.execute( result = await session.execute(select(Tariff.name, Tariff.subgroup_title).where(Tariff.id == key_obj.tariff_id))
select(Tariff.name, Tariff.subgroup_title).where(Tariff.id == key_details["tariff_id"])
)
row = result.first() row = result.first()
if row: if row:
tariff_name = row[0] tariff_name = row[0]
@@ -590,20 +586,18 @@ async def handle_key_edit(
text = ( text = (
"<b>🔑 Информация о подписке</b>\n\n" "<b>🔑 Информация о подписке</b>\n\n"
"<blockquote>" "<blockquote>"
f"🔗 <b>Ключ:</b> <code>{key_value}</code>\n" f"🔗 <b>Ключ{alias_part}:</b> <code> {key_value}</code>\n"
f"📆 <b>Создан:</b> {created_at} (МСК)\n" f"📆 <b>Создан:</b> {created_at} (МСК)\n"
f"⏰ <b>Истекает:</b> {expiry_date} (МСК)\n" f"⏰ <b>Истекает:</b> {expiry_date} (МСК)\n"
f"🌐 <b>Кластер:</b> {key_details.get('cluster_name', '')}\n" f"🌐 <b>Кластер:</b> {key_obj.server_id or ''}\n"
f"🆔 <b>ID клиента:</b> {key_details.get('tg_id', '')}\n" f"🆔 <b>ID клиента:</b> {key_obj.tg_id or ''}\n"
f"📁 <b>Группа:</b> {subgroup_title}\n" f"📁 <b>Группа:</b> {subgroup_title}\n"
f"📦 <b>Тариф:</b> {tariff_name}\n" f"📦 <b>Тариф:</b> {tariff_name}\n"
"</blockquote>"
) )
if alias:
text += f"🏷️ <b>Имя подписки:</b> {alias}\n"
text += "</blockquote>"
if not update or not callback_data.edit: if not update or not callback_data.edit:
await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email)) await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_obj.__dict__, email))
else: else:
await callback_query.message.edit_text( await callback_query.message.edit_text(
text=text, text=text,
@@ -1163,7 +1157,7 @@ async def process_user_search(
result_referrer = await session.execute(stmt_referrer) result_referrer = await session.execute(stmt_referrer)
ref_username = result_referrer.scalar_one_or_none() ref_username = result_referrer.scalar_one_or_none()
if ref_username: if ref_username:
referrer_text = f"🤝 Пригласил: @{ref_username}" referrer_text = f"🤝 Пригласил: @{ref_username} ({referrer_tg_id})"
else: else:
referrer_text = f"🤝 Пригласил: {referrer_tg_id}" referrer_text = f"🤝 Пригласил: {referrer_tg_id}"
@@ -1201,29 +1195,18 @@ async def process_user_search(
if referrer_text: if referrer_text:
body += Text(referrer_text, "\n") body += Text(referrer_text, "\n")
text_builder = Text( text_builder = Text(Bold("📊 Информация о пользователе"), "\n\n", BlockQuote(body))
Bold("📊 Информация о пользователе"), "\n\n",
BlockQuote(body)
)
text = text_builder.as_html() text = text_builder.as_html()
kb = await build_user_edit_kb(tg_id, key_records, is_banned=is_banned) kb = await build_user_edit_kb(tg_id, key_records, is_banned=is_banned)
if edit: if edit:
try: try:
await message.edit_text( await message.edit_text(text=text, reply_markup=kb, disable_web_page_preview=True)
text=text,
reply_markup=kb,
disable_web_page_preview=True
)
except TelegramBadRequest: except TelegramBadRequest:
pass pass
else: else:
await message.answer( await message.answer(text=text, reply_markup=kb, disable_web_page_preview=True)
text=text,
reply_markup=kb,
disable_web_page_preview=True
)
async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession) -> Exception | None: async def change_expiry_time(expiry_time: int, email: str, session: AsyncSession) -> Exception | None:
+9 -6
View File
@@ -10,7 +10,7 @@ CANCEL = "❌ Отмена"
# Профиль # Профиль
ADD_SUB = " Добавить новую подписку" ADD_SUB = "Купить новую подписку"
MY_SUBS = "📱 Мои подписки" MY_SUBS = "📱 Мои подписки"
BALANCE = "💵 Баланс" BALANCE = "💵 Баланс"
INVITE = "👥 Пригласить" INVITE = "👥 Пригласить"
@@ -37,6 +37,7 @@ DISCOUNT_TARIFF = "🔥 Получить скидку"
MAX_DISCOUNT_TARIFF = "⚡ Получить максимальную скидку" MAX_DISCOUNT_TARIFF = "⚡ Получить максимальную скидку"
# Кнопки подписки на канал # Кнопки подписки на канал
SUB_CHANELL = "📢 Подписаться" SUB_CHANELL = "📢 Подписаться"
SUB_CHANELL_DONE = "✅ Я подписался" SUB_CHANELL_DONE = "✅ Я подписался"
@@ -57,7 +58,7 @@ DOWNLOAD_PC_BUTTON = "💻 Скачать Windows"
IMPORT_IOS = "🍏 Подключить" IMPORT_IOS = "🍏 Подключить"
IMPORT_ANDROID = "🤖 Подключить" IMPORT_ANDROID = "🤖 Подключить"
PC_BUTTON = "💻 Компьютеры" PC_BUTTON = "💻 Компьютеры"
TV_BUTTON = "📺 Андроид TV" TV_BUTTON = "📺 Подключить Андроид TV"
CONNECT_PHONE = "📱 Подключить телефон" CONNECT_PHONE = "📱 Подключить телефон"
CONNECT_DEVICE = "📲 Подключить устройство" CONNECT_DEVICE = "📲 Подключить устройство"
CONNECT_WINDOWS_BUTTON = "💻 Подключить" CONNECT_WINDOWS_BUTTON = "💻 Подключить"
@@ -65,8 +66,6 @@ CONNECT_MACOS_BUTTON = "🍏 Подключить"
ALIAS = "✏️" ALIAS = "✏️"
UNFREEZE = "🟢 Разморозить подписку" UNFREEZE = "🟢 Разморозить подписку"
FREEZE = "🛑 Заморозить подписку" FREEZE = "🛑 Заморозить подписку"
RENEW = "⏳ Продлить"
RENEW_FULL = "⏳ Продлить подписку"
DELETE = "❌ Удалить" DELETE = "❌ Удалить"
CHANGE_LOCATION = "🌍 Сменить локацию" CHANGE_LOCATION = "🌍 Сменить локацию"
QR = "📷 Показать QR-код" QR = "📷 Показать QR-код"
@@ -78,7 +77,7 @@ PC_MACOS = "🍏 macOS"
TV = "📺 Телевизор" TV = "📺 Телевизор"
ROUTER = "📶 Роутер" ROUTER = "📶 Роутер"
MANUAL_INSTRUCTIONS = "📖 Ручная установка" MANUAL_INSTRUCTIONS = "📖 Ручная установка"
RENEW_KEY = "Купить VPN 🔄 Продлить VPN" RENEW_KEY = " Продлить подписку"
RENEW_KEY_NOTIFICATION = "🔄 Продлить подписку" RENEW_KEY_NOTIFICATION = "🔄 Продлить подписку"
TV_CONTINUE = "▶ Продолжить" TV_CONTINUE = "▶ Продолжить"
TV_INSTRUCTIONS = "📖 Полная инструкция" TV_INSTRUCTIONS = "📖 Полная инструкция"
@@ -91,12 +90,16 @@ PAY = "Пополнить"
PAY_2 = "Оплатить" PAY_2 = "Оплатить"
CUSTOM_AMOUNT = "💰 Ввести свою сумму" CUSTOM_AMOUNT = "💰 Ввести свою сумму"
STARS_BOT = "🤖 Бот для покупки звезд" STARS_BOT = "🤖 Бот для покупки звезд"
WATA = "💳 WATA"
WATA_RU = "🇷🇺 WATA: Карты РФ" WATA_RU = "🇷🇺 WATA: Карты РФ"
WATA_SBP = "🏦 WATA: СБП" WATA_SBP = "🏦 WATA: СБП"
WATA_INT = "🌍 WATA: Международные карты" WATA_INT = "🌍 WATA: Международные карты"
KASSAI_CARDS = "💳 KassaAI: Карты РФ" KASSAI_CARDS = "💳 KassaAI: Карты РФ"
KASSAI_SBP = "🏦 KassaAI: СБП" KASSAI_SBP = "🏦 KassaAI: СБП"
KASSAI = "💳 KassaAI"
TRIBUTE = "💳 Tribute" TRIBUTE = "💳 Tribute"
RUB_CURRENCY = "₽ Рубли (RUB)"
USD_CURRENCY = "$ USD / Cryptowallet"
# Кнопки Heleket # Кнопки Heleket
HELEKET_CRYPTO = "Heleket: Crypto" HELEKET = "Heleket Crypto"
+4 -1
View File
@@ -29,6 +29,7 @@ from database import (
) )
from handlers.buttons import MAIN_MENU from handlers.buttons import MAIN_MENU
from handlers.keys.operations import renew_key_in_cluster from handlers.keys.operations import renew_key_in_cluster
from handlers.payments.currency_rates import format_for_user
from handlers.profile import process_callback_view_profile from handlers.profile import process_callback_view_profile
from handlers.texts import ( from handlers.texts import (
COUPONS_DAYS_MESSAGE, COUPONS_DAYS_MESSAGE,
@@ -98,6 +99,7 @@ async def activate_coupon(
return return
user = user_data or message.from_user or message.chat user = user_data or message.from_user or message.chat
language_code = user.get("language_code") if isinstance(user, dict) else getattr(user, "language_code", None)
user_id = user["tg_id"] if isinstance(user, dict) else user.id user_id = user["tg_id"] if isinstance(user, dict) else user.id
usage = await check_coupon_usage(session, coupon.id, user_id) usage = await check_coupon_usage(session, coupon.id, user_id)
@@ -127,7 +129,8 @@ async def activate_coupon(
await update_coupon_usage_count(session, coupon.id) await update_coupon_usage_count(session, coupon.id)
await create_coupon_usage(session, coupon.id, user_id) await create_coupon_usage(session, coupon.id, user_id)
await add_payment(session, tg_id=user_id, amount=coupon.amount, payment_system="coupon") await add_payment(session, tg_id=user_id, amount=coupon.amount, payment_system="coupon")
await message.answer(f"✅ Купон активирован, на баланс начислено {coupon.amount} рублей.") amount_txt = await format_for_user(session, user_id, coupon.amount, language_code)
await message.answer(f"✅ Купон активирован, на баланс начислено {amount_txt}.")
await state.clear() await state.clear()
except Exception as e: except Exception as e:
logger.error(f"Ошибка при активации купона на баланс: {e}") logger.error(f"Ошибка при активации купона на баланс: {e}")
+3 -3
View File
@@ -3,10 +3,10 @@ from aiogram.types import InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import SUPPORT_CHAT_URL from config import SUPPORT_CHAT_URL
from database import async_session_maker
from handlers.buttons import MAIN_MENU, SUPPORT from handlers.buttons import MAIN_MENU, SUPPORT
from handlers.texts import FALLBACK_MESSAGE from handlers.texts import FALLBACK_MESSAGE
from hooks.hooks import run_hooks from hooks.hooks import run_hooks
from database import async_session_maker
fallback_router = Router() fallback_router = Router()
@@ -23,9 +23,9 @@ async def handle_unhandled_messages(message: Message):
first_name=message.from_user.first_name, first_name=message.from_user.first_name,
last_name=message.from_user.last_name, last_name=message.from_user.last_name,
session=session, session=session,
message=message message=message,
) )
keyboard = InlineKeyboardBuilder() keyboard = InlineKeyboardBuilder()
keyboard.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL)) keyboard.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
keyboard.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) keyboard.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
+5 -3
View File
@@ -38,8 +38,8 @@ from handlers.texts import (
SUBSCRIPTION_DESCRIPTION, SUBSCRIPTION_DESCRIPTION,
) )
from handlers.utils import edit_or_send_message from handlers.utils import edit_or_send_message
from hooks.hooks import run_hooks
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from logger import logger from logger import logger
@@ -63,10 +63,12 @@ async def handle_connect_device(callback_query: CallbackQuery, session: AsyncSes
hook_builder = InlineKeyboardBuilder() hook_builder = InlineKeyboardBuilder()
hook_builder.attach(builder) hook_builder.attach(builder)
hook_commands = await run_hooks("connect_device_menu", chat_id=callback_query.from_user.id, admin=False, session=session) hook_commands = await run_hooks(
"connect_device_menu", chat_id=callback_query.from_user.id, admin=False, session=session
)
if hook_commands: if hook_commands:
hook_builder = insert_hook_buttons(hook_builder, hook_commands) hook_builder = insert_hook_buttons(hook_builder, hook_commands)
final_markup = hook_builder.as_markup() final_markup = hook_builder.as_markup()
except Exception as e: except Exception as e:
logger.warning(f"[CONNECT_DEVICE] Ошибка при применении хуков: {e}") logger.warning(f"[CONNECT_DEVICE] Ошибка при применении хуков: {e}")
+4 -2
View File
@@ -40,8 +40,8 @@ from handlers.utils import (
get_least_loaded_cluster, get_least_loaded_cluster,
is_full_remnawave_cluster, is_full_remnawave_cluster,
) )
from hooks.hooks import run_hooks
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from logger import logger from logger import logger
@@ -173,7 +173,9 @@ async def key_cluster_mode(
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
try: try:
hook_commands = await run_hooks("key_creation_complete", chat_id=tg_id, admin=False, session=session, email=email, key_name=key_name) hook_commands = await run_hooks(
"key_creation_complete", chat_id=tg_id, admin=False, session=session, email=email, key_name=key_name
)
if hook_commands: if hook_commands:
builder = insert_hook_buttons(builder, hook_commands) builder = insert_hook_buttons(builder, hook_commands)
except Exception as e: except Exception as e:
+5 -3
View File
@@ -44,11 +44,11 @@ from handlers.utils import (
get_least_loaded_cluster, get_least_loaded_cluster,
is_full_remnawave_cluster, is_full_remnawave_cluster,
) )
from hooks.hooks import run_hooks
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from logger import logger from logger import logger
from panels._3xui import delete_client, get_xui_instance
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import delete_client, get_xui_instance
router = Router() router = Router()
@@ -513,7 +513,9 @@ async def finalize_key_creation(
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
try: try:
hook_commands = await run_hooks("key_creation_complete", chat_id=tg_id, admin=False, session=session, email=email, key_name=key_name) hook_commands = await run_hooks(
"key_creation_complete", chat_id=tg_id, admin=False, session=session, email=email, key_name=key_name
)
if hook_commands: if hook_commands:
builder = insert_hook_buttons(builder, hook_commands) builder = insert_hook_buttons(builder, hook_commands)
except Exception as e: except Exception as e:
+57 -64
View File
@@ -12,39 +12,34 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select from sqlalchemy import select
from config import ( from config import (
DISCOUNT_ACTIVE_HOURS,
NOTIFY_EXTRA_DAYS, NOTIFY_EXTRA_DAYS,
TRIAL_TIME_DISABLE, TRIAL_TIME_DISABLE,
USE_COUNTRY_SELECTION, USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW, USE_NEW_PAYMENT_FLOW,
DISCOUNT_ACTIVE_HOURS,
) )
from database import ( from database import (
add_user, add_user,
check_user_exists, check_user_exists,
create_temporary_data,
get_balance, get_balance,
get_tariff_by_id, get_tariff_by_id,
get_tariffs_for_cluster, get_tariffs_for_cluster,
get_trial, get_trial,
) )
from database.notifications import check_hot_lead_discount
from database.models import Admin from database.models import Admin
from database.notifications import check_hot_lead_discount
from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs
from handlers.admin.panel.keyboard import AdminPanelCallback from handlers.admin.panel.keyboard import AdminPanelCallback
from handlers.buttons import MAIN_MENU, PAYMENT from handlers.buttons import MAIN_MENU, PAYMENT
from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.currency_rates import format_for_user
from handlers.payments.stars_pay import process_custom_amount_input_stars from handlers.payments.fast_payment_flow import try_fast_payment_flow
from handlers.payments.wata import handle_custom_amount_input as handle_custom_amount_input_wata
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.payments.yoomoney_pay import process_custom_amount_input_yoomoney
from handlers.texts import ( from handlers.texts import (
CREATING_CONNECTION_MSG, CREATING_CONNECTION_MSG,
INSUFFICIENT_FUNDS_MSG, INSUFFICIENT_FUNDS_MSG,
SELECT_TARIFF_PLAN_MSG, SELECT_TARIFF_PLAN_MSG,
) )
from handlers.utils import edit_or_send_message, get_least_loaded_cluster, format_discount_time_left from handlers.utils import edit_or_send_message, format_discount_time_left, get_least_loaded_cluster
from logger import logger from logger import logger
from utils.modules_loader import load_module_fast_flow_handlers
from .key_cluster_mode import key_cluster_mode from .key_cluster_mode import key_cluster_mode
from .key_country_mode import key_country_mode from .key_country_mode import key_country_mode
@@ -144,6 +139,12 @@ async def handle_key_creation(
tariffs = await get_tariffs_for_cluster(session, cluster_name) tariffs = await get_tariffs_for_cluster(session, cluster_name)
language_code = (
message_or_query.from_user.language_code
if not isinstance(message_or_query, CallbackQuery)
else message_or_query.from_user.language_code
)
discount_info = None discount_info = None
subgroup_weights = {} subgroup_weights = {}
@@ -151,17 +152,18 @@ async def handle_key_creation(
group_code = tariffs[0].get("group_code") group_code = tariffs[0].get("group_code")
if group_code: if group_code:
from database.notifications import check_hot_lead_discount from database.notifications import check_hot_lead_discount
discount_info = await check_hot_lead_discount(session, tg_id) discount_info = await check_hot_lead_discount(session, tg_id)
if discount_info and discount_info.get("available"): if discount_info and discount_info.get("available"):
group_code = discount_info["tariff_group"] group_code = discount_info["tariff_group"]
await state.update_data(discount_info=discount_info) await state.update_data(discount_info=discount_info)
else: else:
await state.update_data(discount_info=None) await state.update_data(discount_info=None)
tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True)
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] tariffs = [t for t in tariffs_data["tariffs"] if t.get("is_active")]
subgroup_weights = tariffs_data['subgroup_weights'] subgroup_weights = tariffs_data["subgroup_weights"]
if not tariffs: if not tariffs:
result = await session.execute(select(Admin).where(Admin.tg_id == tg_id)) result = await session.execute(select(Admin).where(Admin.tg_id == tg_id))
@@ -216,18 +218,19 @@ async def handle_key_creation(
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for t in grouped_tariffs.get(None, []): for t in grouped_tariffs.get(None, []):
price_txt = await format_for_user(session, tg_id, t.get("price_rub", 0), language_code)
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{t['name']}{t['price_rub']}", text=f"{t['name']}{price_txt}",
callback_data=f"select_tariff_plan|{t['id']}", callback_data=f"select_tariff_plan|{t['id']}",
) )
) )
sorted_subgroups = sorted( sorted_subgroups = sorted(
[k for k in grouped_tariffs if k], [k for k in grouped_tariffs if k],
key=lambda x: (subgroup_weights.get(x, 999999) if subgroup_weights else 999999, x) key=lambda x: (subgroup_weights.get(x, 999999) if subgroup_weights else 999999, x),
) )
for subgroup in sorted_subgroups: for subgroup in sorted_subgroups:
subgroup_hash = create_subgroup_hash(subgroup, group_code) subgroup_hash = create_subgroup_hash(subgroup, group_code)
builder.row( builder.row(
@@ -242,16 +245,16 @@ async def handle_key_creation(
target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
discount_message = "" discount_message = ""
if discount_info and discount_info.get("available"): if discount_info and discount_info.get("available"):
discount_message = f"\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>" discount_message = "\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2": if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b>\n" discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b>\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n" discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else: else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b>\n" discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b>\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n" discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n"
expires_at = discount_info["expires_at"] expires_at = discount_info["expires_at"]
discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}</b>, не упустите свой шанс!" discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}</b>, не упустите свой шанс!"
@@ -291,7 +294,7 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte
group_code = tariffs[0].get("group_code") group_code = tariffs[0].get("group_code")
if group_code: if group_code:
tariffs = await get_tariffs(session, group_code=group_code) tariffs = await get_tariffs(session, group_code=group_code)
filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup and t.get('is_active')] filtered = [t for t in tariffs if t.get("subgroup_title") == subgroup and t.get("is_active")]
if not filtered: if not filtered:
await edit_or_send_message( await edit_or_send_message(
@@ -301,11 +304,15 @@ async def show_tariffs_in_subgroup_user(callback: CallbackQuery, state: FSMConte
) )
return return
tg_id = callback.from_user.id
language_code = callback.from_user.language_code
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for t in filtered: for t in filtered:
price_txt = await format_for_user(session, tg_id, t.get("price_rub", 0), language_code)
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{t['name']}{t['price_rub']}", text=f"{t['name']}{price_txt}",
callback_data=f"select_tariff_plan|{t['id']}", callback_data=f"select_tariff_plan|{t['id']}",
) )
) )
@@ -365,49 +372,35 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
if balance < price_rub: if balance < price_rub:
required_amount = ceil(price_rub - balance) required_amount = ceil(price_rub - balance)
await create_temporary_data(
session, if USE_NEW_PAYMENT_FLOW:
tg_id, handled = await try_fast_payment_flow(
"waiting_for_payment", callback_query,
{ session,
"tariff_id": tariff_id, state,
"duration_days": duration_days, tg_id=tg_id,
"required_amount": required_amount, temp_key="waiting_for_payment",
}, temp_payload={
"tariff_id": tariff_id,
"duration_days": duration_days,
"required_amount": required_amount,
},
required_amount=required_amount,
)
if handled:
return
language_code = getattr(callback_query.from_user, "language_code", None)
required_amount_text = await format_for_user(session, tg_id, float(required_amount), language_code)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text=INSUFFICIENT_FUNDS_MSG.format(required_amount=required_amount_text),
reply_markup=builder.as_markup(),
) )
module_fast_flow_handlers = load_module_fast_flow_handlers()
flow_handled = False
if USE_NEW_PAYMENT_FLOW in module_fast_flow_handlers:
try:
handler = module_fast_flow_handlers[USE_NEW_PAYMENT_FLOW]
await handler(callback_query, session, state)
flow_handled = True
except Exception as e:
logger.error(f"[CREATE] Ошибка в модульном обработчике быстрого флоу {USE_NEW_PAYMENT_FLOW}: {e}")
if not flow_handled:
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
await handle_custom_amount_input(message=callback_query, session=session)
elif USE_NEW_PAYMENT_FLOW == "STARS":
await process_custom_amount_input_stars(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "YOOMONEY":
await process_custom_amount_input_yoomoney(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "WATA":
await state.update_data(wata_cassa="sbp", required_amount=required_amount)
await handle_custom_amount_input_wata(callback_query, state)
else:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text=INSUFFICIENT_FUNDS_MSG.format(required_amount=required_amount),
reply_markup=builder.as_markup(),
)
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
+11 -17
View File
@@ -7,12 +7,12 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import DISCOUNT_ACTIVE_HOURS from config import DISCOUNT_ACTIVE_HOURS
from database import get_tariffs, get_keys from database import get_keys, get_tariffs
from database.models import Notification from database.models import Notification
from handlers.utils import format_discount_time_left from handlers.buttons import MAIN_MENU, RENEW_KEY_NOTIFICATION
from handlers.notifications.notify_kb import build_tariffs_keyboard from handlers.notifications.notify_kb import build_tariffs_keyboard
from handlers.texts import DISCOUNT_TARIFF, DISCOUNT_TARIFF_MAX from handlers.texts import DISCOUNT_TARIFF, DISCOUNT_TARIFF_MAX
from handlers.buttons import RENEW_KEY_NOTIFICATION, MAIN_MENU from handlers.utils import format_discount_time_left
from logger import logger from logger import logger
from .key_create import select_tariff_plan from .key_create import select_tariff_plan
@@ -43,15 +43,12 @@ async def handle_discount_entry(callback: CallbackQuery, session: AsyncSession):
return return
keys = await get_keys(session, tg_id) keys = await get_keys(session, tg_id)
if keys and len(keys) > 0: if keys and len(keys) > 0:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(text=RENEW_KEY_NOTIFICATION, callback_data=f"renew_key|{keys[0].email}"))
text=RENEW_KEY_NOTIFICATION,
callback_data=f"renew_key|{keys[0].email}"
))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
expires_at = last_time + timedelta(hours=DISCOUNT_ACTIVE_HOURS) expires_at = last_time + timedelta(hours=DISCOUNT_ACTIVE_HOURS)
await callback.message.edit_text( await callback.message.edit_text(
f"🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n\n<blockquote>" f"🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n\n<blockquote>"
@@ -59,7 +56,7 @@ async def handle_discount_entry(callback: CallbackQuery, session: AsyncSession):
f"🚀 <b>Получите максимум возможностей</b> по выгодной цене!\n" f"🚀 <b>Получите максимум возможностей</b> по выгодной цене!\n"
f"</blockquote>\n" f"</blockquote>\n"
f"⏰ <b>Предложение действует всего: {format_discount_time_left(expires_at, DISCOUNT_ACTIVE_HOURS)} — не упустите свой шанс!</b>", f"⏰ <b>Предложение действует всего: {format_discount_time_left(expires_at, DISCOUNT_ACTIVE_HOURS)} — не упустите свой шанс!</b>",
reply_markup=builder.as_markup() reply_markup=builder.as_markup(),
) )
else: else:
tariffs = await get_tariffs(session=session, group_code="discounts") tariffs = await get_tariffs(session=session, group_code="discounts")
@@ -113,22 +110,19 @@ async def handle_ultra_discount(callback: CallbackQuery, session: AsyncSession):
return return
keys = await get_keys(session, tg_id) keys = await get_keys(session, tg_id)
if keys and len(keys) > 0: if keys and len(keys) > 0:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton( builder.row(InlineKeyboardButton(text=RENEW_KEY_NOTIFICATION, callback_data=f"renew_key|{keys[0].email}"))
text=RENEW_KEY_NOTIFICATION,
callback_data=f"renew_key|{keys[0].email}"
))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await callback.message.edit_text( await callback.message.edit_text(
f"🎯 <b>УНИКАЛЬНОЕ ФИНАЛЬНОЕ ПРЕДЛОЖЕНИЕ!</b>\n\n<blockquote>" f"🎯 <b>УНИКАЛЬНОЕ ФИНАЛЬНОЕ ПРЕДЛОЖЕНИЕ!</b>\n\n<blockquote>"
f"💎 <b>Доступ к тарифам с МАКСИМАЛЬНОЙ выгодой</b> — только для вас!\n" f"💎 <b>Доступ к тарифам с МАКСИМАЛЬНОЙ выгодой</b> — только для вас!\n"
f"🚀 <b>Уникальные условия</b> — получите максимум преимуществ по минимальной цене!\n" f"🚀 <b>Уникальные условия</b> — получите максимум преимуществ по минимальной цене!\n"
f"</blockquote>\n" f"</blockquote>\n"
f"⏰ <b>Время ограничено: {format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)} — не упустите шанс!</b>", f"⏰ <b>Время ограничено: {format_discount_time_left(last_time, DISCOUNT_ACTIVE_HOURS)} — не упустите шанс!</b>",
reply_markup=builder.as_markup() reply_markup=builder.as_markup(),
) )
else: else:
tariffs = await get_tariffs(session, group_code="discounts_max") tariffs = await get_tariffs(session, group_code="discounts_max")
+74 -85
View File
@@ -13,10 +13,9 @@ from sqlalchemy import or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from bot import bot from bot import bot
from config import USE_NEW_PAYMENT_FLOW, DISCOUNT_ACTIVE_HOURS from config import DISCOUNT_ACTIVE_HOURS, USE_NEW_PAYMENT_FLOW
from database import ( from database import (
check_tariff_exists, check_tariff_exists,
create_temporary_data,
get_balance, get_balance,
get_key_by_server, get_key_by_server,
get_key_details, get_key_details,
@@ -30,22 +29,18 @@ from database.notifications import check_hot_lead_discount
from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs from database.tariffs import create_subgroup_hash, find_subgroup_by_hash, get_tariffs
from handlers.buttons import BACK, MAIN_MENU, MY_SUB, PAYMENT from handlers.buttons import BACK, MAIN_MENU, MY_SUB, PAYMENT
from handlers.keys.operations import renew_key_in_cluster from handlers.keys.operations import renew_key_in_cluster
from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.currency_rates import format_for_user
from handlers.payments.stars_pay import process_custom_amount_input_stars from handlers.payments.fast_payment_flow import try_fast_payment_flow
from handlers.payments.wata import handle_custom_amount_input as handle_custom_amount_input_wata
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.payments.yoomoney_pay import process_custom_amount_input_yoomoney
from handlers.texts import ( from handlers.texts import (
INSUFFICIENT_FUNDS_RENEWAL_MSG, INSUFFICIENT_FUNDS_RENEWAL_MSG,
KEY_NOT_FOUND_MSG, KEY_NOT_FOUND_MSG,
PLAN_SELECTION_MSG, PLAN_SELECTION_MSG,
get_renewal_message, get_renewal_message,
) )
from handlers.utils import edit_or_send_message, get_russian_month, format_discount_time_left from handlers.utils import edit_or_send_message, format_discount_time_left, get_russian_month
from hooks.hooks import run_hooks
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from logger import logger from logger import logger
from utils.modules_loader import load_module_fast_flow_handlers
router = Router() router = Router()
@@ -101,14 +96,14 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
group_code = current_tariff["group_code"] group_code = current_tariff["group_code"]
discount_info = await check_hot_lead_discount(session, tg_id) discount_info = await check_hot_lead_discount(session, tg_id)
if discount_info.get("available"): if discount_info.get("available"):
group_code = discount_info["tariff_group"] group_code = discount_info["tariff_group"]
tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True) tariffs_data = await get_tariffs(session, group_code=group_code, with_subgroup_weights=True)
tariffs = [t for t in tariffs_data['tariffs'] if t.get('is_active')] tariffs = [t for t in tariffs_data["tariffs"] if t.get("is_active")]
subgroup_weights = tariffs_data['subgroup_weights'] subgroup_weights = tariffs_data["subgroup_weights"]
if not tariffs: if not tariffs:
await callback_query.message.answer("❌ Нет доступных тарифов для продления.") await callback_query.message.answer("❌ Нет доступных тарифов для продления.")
return return
@@ -120,19 +115,19 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
language_code = getattr(callback_query.from_user, "language_code", None)
for t in grouped_tariffs.get(None, []): for t in grouped_tariffs.get(None, []):
price_text = await format_for_user(session, tg_id, t["price_rub"], language_code)
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{t['name']}{t['price_rub']}", text=f"{t['name']}{price_text}",
callback_data=f"renew_plan|{t['id']}", callback_data=f"renew_plan|{t['id']}",
) )
) )
sorted_subgroups = sorted( sorted_subgroups = sorted([k for k in grouped_tariffs if k], key=lambda x: (subgroup_weights.get(x, 999999), x))
[k for k in grouped_tariffs if k],
key=lambda x: (subgroup_weights.get(x, 999999), x)
)
for subgroup in sorted_subgroups: for subgroup in sorted_subgroups:
subgroup_hash = create_subgroup_hash(subgroup, group_code) subgroup_hash = create_subgroup_hash(subgroup, group_code)
builder.row( builder.row(
@@ -142,7 +137,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
) )
) )
builder.row(InlineKeyboardButton(text=BACK, callback_data="renew_menu")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{key_name}"))
try: try:
hook_builder = InlineKeyboardBuilder() hook_builder = InlineKeyboardBuilder()
@@ -151,31 +146,35 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
hook_commands = await run_hooks("renew_tariffs", chat_id=tg_id, admin=False, session=session) hook_commands = await run_hooks("renew_tariffs", chat_id=tg_id, admin=False, session=session)
if hook_commands: if hook_commands:
hook_builder = insert_hook_buttons(hook_builder, hook_commands) hook_builder = insert_hook_buttons(hook_builder, hook_commands)
final_markup = hook_builder.as_markup() final_markup = hook_builder.as_markup()
except Exception as e: except Exception as e:
logger.warning(f"[RENEW] Ошибка при применении хуков: {e}") logger.warning(f"[RENEW] Ошибка при применении хуков: {e}")
final_markup = builder.as_markup() final_markup = builder.as_markup()
balance = await get_balance(session, tg_id) balance_rub = await get_balance(session, tg_id) or 0
balance = await format_for_user(session, tg_id, balance_rub, language_code)
discount_message = "" discount_message = ""
if discount_info.get("available"): if discount_info.get("available"):
discount_message = f"\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>" discount_message = "\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2": if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n" discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n" discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else: else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n" discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n" discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n"
expires_at = discount_info["expires_at"] expires_at = discount_info["expires_at"]
discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>" discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>"
response_message = PLAN_SELECTION_MSG.format( response_message = (
balance=balance, PLAN_SELECTION_MSG.format(
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"), balance=balance,
) + discount_message expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
+ discount_message
)
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
@@ -230,8 +229,9 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
group_code = row[0] group_code = row[0]
tg_id = callback.from_user.id tg_id = callback.from_user.id
language_code = callback.from_user.language_code
discount_info = await check_hot_lead_discount(session, tg_id) discount_info = await check_hot_lead_discount(session, tg_id)
if discount_info.get("available"): if discount_info.get("available"):
group_code = discount_info["tariff_group"] group_code = discount_info["tariff_group"]
@@ -253,9 +253,10 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for t in filtered: for t in filtered:
price_txt = await format_for_user(session, tg_id, t.get("price_rub", 0), language_code)
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=f"{t['name']}{t['price_rub']}", text=f"{t['name']}{price_txt}",
callback_data=f"renew_plan|{t['id']}", callback_data=f"renew_plan|{t['id']}",
) )
) )
@@ -267,10 +268,12 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
hook_builder = InlineKeyboardBuilder() hook_builder = InlineKeyboardBuilder()
hook_builder.attach(builder) hook_builder.attach(builder)
hook_commands = await run_hooks("renew_tariffs", chat_id=callback.from_user.id, admin=False, session=session) hook_commands = await run_hooks(
"renew_tariffs", chat_id=callback.from_user.id, admin=False, session=session
)
if hook_commands: if hook_commands:
hook_builder = insert_hook_buttons(hook_builder, hook_commands) hook_builder = insert_hook_buttons(hook_builder, hook_commands)
final_markup = hook_builder.as_markup() final_markup = hook_builder.as_markup()
except Exception as e: except Exception as e:
logger.warning(f"[RENEW_SUBGROUP] Ошибка при применении хуков: {e}") logger.warning(f"[RENEW_SUBGROUP] Ошибка при применении хуков: {e}")
@@ -278,14 +281,14 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
discount_message = "" discount_message = ""
if discount_info.get("available"): if discount_info.get("available"):
discount_message = f"\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>" discount_message = "\n\n🎯 <b>ЭКСКЛЮЗИВНОЕ ПРЕДЛОЖЕНИЕ!</b>\n<blockquote>"
if discount_info["type"] == "hot_lead_step_2": if discount_info["type"] == "hot_lead_step_2":
discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n" discount_message += "💎 <b>Вам открыт доступ к специальным тарифам</b> для продления\n"
discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n" discount_message += "🚀 <b>Эксклюзивные предложения</b> - доступны только для вас!\n"
else: else:
discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n" discount_message += "💎 <b>Вам открыт доступ к МАКСИМАЛЬНО выгодным тарифам</b> для продления\n"
discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n" discount_message += "🚀 <b>VIP предложения</b> - максимальная выгода!\n"
expires_at = discount_info["expires_at"] expires_at = discount_info["expires_at"]
discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>" discount_message += f"</blockquote>\n⏰ <b>Предложение действует только: {format_discount_time_left(expires_at - timedelta(hours=DISCOUNT_ACTIVE_HOURS), DISCOUNT_ACTIVE_HOURS)}, не упустите свой шанс!</b>"
@@ -325,10 +328,9 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]: if not discount_info.get("available") or datetime.utcnow() >= discount_info["expires_at"]:
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await callback_query.message.answer( await callback_query.message.answer(
"❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.", "❌ Скидка недоступна или истекла. Пожалуйста, выберите тариф заново.",
reply_markup=builder.as_markup() reply_markup=builder.as_markup(),
) )
return return
@@ -353,57 +355,42 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, state: FSMC
balance = round(await get_balance(session, tg_id), 2) balance = round(await get_balance(session, tg_id), 2)
cost = round(cost, 2) cost = round(cost, 2)
if balance < cost: if balance < cost:
required_amount = ceil(cost - balance) required_amount = ceil(cost - balance)
logger.info(f"[RENEW] Недостаточно средств: {required_amount}")
await create_temporary_data( if USE_NEW_PAYMENT_FLOW:
session, handled = await try_fast_payment_flow(
tg_id, callback_query,
"waiting_for_renewal_payment", session,
{ state,
"tariff_id": tariff_id, tg_id=tg_id,
"client_id": client_id, temp_key="waiting_for_renewal_payment",
"cost": cost, temp_payload={
"required_amount": required_amount, "tariff_id": tariff_id,
"new_expiry_time": new_expiry_time, "client_id": client_id,
"total_gb": total_gb, "cost": cost,
"email": email, "required_amount": required_amount,
}, "new_expiry_time": new_expiry_time,
"total_gb": total_gb,
"email": email,
},
required_amount=required_amount,
)
if handled:
return
language_code = getattr(callback_query.from_user, "language_code", None)
required_amount_text = await format_for_user(session, tg_id, float(required_amount), language_code)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount_text),
reply_markup=builder.as_markup(),
) )
module_fast_flow_handlers = load_module_fast_flow_handlers()
flow_handled = False
if USE_NEW_PAYMENT_FLOW in module_fast_flow_handlers:
try:
handler = module_fast_flow_handlers[USE_NEW_PAYMENT_FLOW]
await handler(callback_query, session, state)
flow_handled = True
except Exception as e:
logger.error(f"[RENEW] Ошибка в модульном обработчике быстрого флоу {USE_NEW_PAYMENT_FLOW}: {e}")
if not flow_handled:
if USE_NEW_PAYMENT_FLOW == "YOOKASSA":
await process_custom_amount_input(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "ROBOKASSA":
await handle_custom_amount_input(message=callback_query, session=session)
elif USE_NEW_PAYMENT_FLOW == "STARS":
await process_custom_amount_input_stars(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "YOOMONEY":
await process_custom_amount_input_yoomoney(callback_query, session)
elif USE_NEW_PAYMENT_FLOW == "WATA":
await state.update_data(wata_cassa="sbp", required_amount=required_amount)
await handle_custom_amount_input_wata(callback_query, state)
else:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text=INSUFFICIENT_FUNDS_RENEWAL_MSG.format(required_amount=required_amount),
reply_markup=builder.as_markup(),
)
return return
logger.info(f"[RENEW] Продление ключа для пользователя {tg_id} на {duration_days} дней") logger.info(f"[RENEW] Продление ключа для пользователя {tg_id} на {duration_days} дней")
@@ -457,7 +444,9 @@ async def complete_key_renewal(
builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{email}")) builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{email}"))
try: try:
hook_commands = await run_hooks("renewal_complete", chat_id=tg_id, admin=False, session=session, email=email, client_id=client_id) hook_commands = await run_hooks(
"renewal_complete", chat_id=tg_id, admin=False, session=session, email=email, client_id=client_id
)
if hook_commands: if hook_commands:
builder = insert_hook_buttons(builder, hook_commands) builder = insert_hook_buttons(builder, hook_commands)
except Exception as e: except Exception as e:
+49 -108
View File
@@ -1,3 +1,4 @@
import asyncio
import html import html
import os import os
import re import re
@@ -21,13 +22,15 @@ from config import (
ENABLE_UPDATE_SUBSCRIPTION_BUTTON, ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
HWID_RESET_BUTTON, HWID_RESET_BUTTON,
QRCODE, QRCODE,
REMNAWAVE_LOGIN,
REMNAWAVE_PASSWORD,
RENEW_BUTTON_BEFORE_DAYS,
TOGGLE_CLIENT, TOGGLE_CLIENT,
USE_COUNTRY_SELECTION, USE_COUNTRY_SELECTION,
) )
from database import get_key_details, get_keys, get_servers, get_tariff_by_id from database import get_key_details, get_keys, get_servers, get_tariff_by_id
from database.models import Key from database.models import Key
from handlers.buttons import ( from handlers.buttons import (
ADD_SUB,
ALIAS, ALIAS,
BACK, BACK,
CHANGE_LOCATION, CHANGE_LOCATION,
@@ -39,6 +42,7 @@ from handlers.buttons import (
MAIN_MENU, MAIN_MENU,
PC_BUTTON, PC_BUTTON,
QR, QR,
RENEW_KEY,
RENEW_SUB, RENEW_SUB,
TV_BUTTON, TV_BUTTON,
UNFREEZE, UNFREEZE,
@@ -50,7 +54,6 @@ from handlers.texts import (
KEYS_HEADER, KEYS_HEADER,
NO_SUBSCRIPTIONS_MSG, NO_SUBSCRIPTIONS_MSG,
RENAME_KEY_PROMPT, RENAME_KEY_PROMPT,
SELECT_SUBS,
key_message, key_message,
) )
from handlers.utils import ( from handlers.utils import (
@@ -61,9 +64,10 @@ from handlers.utils import (
get_russian_month, get_russian_month,
is_full_remnawave_cluster, is_full_remnawave_cluster,
) )
from logger import logger
from hooks.hooks import run_hooks
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from logger import logger
from panels.remnawave import RemnawaveAPI
router = Router() router = Router()
@@ -85,6 +89,13 @@ async def process_callback_or_message_view_keys(callback_query_or_message: Messa
try: try:
records = await get_keys(session, tg_id) records = await get_keys(session, tg_id)
if records and len(records) == 1:
key_name = records[0].email
image_path = os.path.join("img", "pic_view.jpg")
await render_key_info(target_message, session, key_name, image_path)
return
inline_keyboard, response_message = build_keys_response(records) inline_keyboard, response_message = build_keys_response(records)
image_path = os.path.join("img", "pic_keys.jpg") image_path = os.path.join("img", "pic_keys.jpg")
@@ -200,16 +211,12 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
async def render_key_info(message: Message, session: Any, key_name: str, image_path: str): async def render_key_info(message: Message, session: Any, key_name: str, image_path: str):
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
from panels.remnawave import RemnawaveAPI
record = await get_key_details(session, key_name) record = await get_key_details(session, key_name)
if not record: if not record:
await message.answer("<b>Информация о подписке не найдена.</b>") await message.answer("<b>Информация о подписке не найдена.</b>")
return return
is_frozen = record["is_frozen"] is_frozen = record["is_frozen"]
record["email"]
client_id = record.get("client_id") client_id = record.get("client_id")
remnawave_link = record.get("remnawave_link") remnawave_link = record.get("remnawave_link")
key = record.get("key") key = record.get("key")
@@ -232,7 +239,9 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
expiry_time = record["expiry_time"] expiry_time = record["expiry_time"]
server_name = record["server_id"] server_name = record["server_id"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
time_left = expiry_date - datetime.utcnow() now = datetime.utcnow()
time_left = expiry_date - now
show_renew_btn = time_left.total_seconds() <= RENEW_BUTTON_BEFORE_DAYS * 86400
if time_left.total_seconds() <= 0: if time_left.total_seconds() <= 0:
days_left_message = DAYS_LEFT_MESSAGE days_left_message = DAYS_LEFT_MESSAGE
@@ -249,14 +258,20 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года" f"{expiry_date.strftime('%d')} {get_russian_month(expiry_date)} {expiry_date.strftime('%Y')} года"
) )
is_full_task = asyncio.create_task(is_full_remnawave_cluster(server_name, session))
tariff_task = (
asyncio.create_task(get_tariff_by_id(session, record["tariff_id"])) if record.get("tariff_id") else None
)
is_full_remnawave = await is_full_task
tariff = await tariff_task if tariff_task else None
hwid_count = 0 hwid_count = 0
is_full_remnawave = await is_full_remnawave_cluster(server_name, session)
if is_full_remnawave and client_id: if is_full_remnawave and client_id:
try: try:
servers = await get_servers(session) servers = await get_servers(session)
remna_server = next( remna_server = next(
(srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), (srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None
None,
) )
if remna_server: if remna_server:
api = RemnawaveAPI(remna_server["api_url"]) api = RemnawaveAPI(remna_server["api_url"])
@@ -270,14 +285,11 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
traffic_limit = 0 traffic_limit = 0
device_limit = 0 device_limit = 0
subgroup_title = "" subgroup_title = ""
tariff = None if tariff:
if record.get("tariff_id"): tariff_name = tariff["name"]
tariff = await get_tariff_by_id(session, record["tariff_id"]) traffic_limit = tariff.get("traffic_limit", 0)
if tariff: device_limit = tariff.get("device_limit", 0)
tariff_name = tariff["name"] subgroup_title = tariff.get("subgroup_title", "")
traffic_limit = tariff.get("traffic_limit", 0)
device_limit = tariff.get("device_limit", 0)
subgroup_title = tariff.get("subgroup_title", "")
tariff_duration = tariff_name tariff_duration = tariff_name
@@ -295,12 +307,7 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
) )
if ENABLE_UPDATE_SUBSCRIPTION_BUTTON: if ENABLE_UPDATE_SUBSCRIPTION_BUTTON:
builder.row( builder.row(InlineKeyboardButton(text=RENEW_SUB, callback_data=f"update_subscription|{key_name}"))
InlineKeyboardButton(
text=RENEW_SUB,
callback_data=f"update_subscription|{key_name}",
)
)
if is_full_remnawave and final_link: if is_full_remnawave and final_link:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link))) builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, web_app=WebAppInfo(url=final_link)))
@@ -314,21 +321,18 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
) )
else: else:
builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}")) builder.row(InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}"))
if show_renew_btn:
builder.row(InlineKeyboardButton(text=RENEW_KEY, callback_data=f"renew_key|{key_name}"))
if HWID_RESET_BUTTON and hwid_count > 0: if HWID_RESET_BUTTON and hwid_count > 0:
builder.row( builder.row(InlineKeyboardButton(text=HWID_BUTTON, callback_data=f"reset_hwid|{key_name}"))
InlineKeyboardButton(
text=HWID_BUTTON,
callback_data=f"reset_hwid|{key_name}",
)
)
if QRCODE: if QRCODE:
builder.row(InlineKeyboardButton(text=QR, callback_data=f"show_qr|{key_name}")) builder.row(InlineKeyboardButton(text=QR, callback_data=f"show_qr|{key_name}"))
if ENABLE_DELETE_KEY_BUTTON: if ENABLE_DELETE_KEY_BUTTON:
builder.row( builder.row(InlineKeyboardButton(text=DELETE, callback_data=f"delete_key|{key_name}"))
InlineKeyboardButton(text=DELETE, callback_data=f"delete_key|{key_name}"),
)
if USE_COUNTRY_SELECTION: if USE_COUNTRY_SELECTION:
builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}")) builder.row(InlineKeyboardButton(text=CHANGE_LOCATION, callback_data=f"change_location|{key_name}"))
@@ -336,10 +340,7 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
if TOGGLE_CLIENT: if TOGGLE_CLIENT:
builder.row(InlineKeyboardButton(text=FREEZE, callback_data=f"freeze_subscription|{key_name}")) builder.row(InlineKeyboardButton(text=FREEZE, callback_data=f"freeze_subscription|{key_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
module_buttons = await run_hooks("view_key_menu", key_name=key_name, session=session) module_buttons = await run_hooks("view_key_menu", key_name=key_name, session=session)
builder = insert_hook_buttons(builder, module_buttons) builder = insert_hook_buttons(builder, module_buttons)
@@ -353,11 +354,12 @@ async def render_key_info(message: Message, session: Any, key_name: str, image_p
@router.callback_query(F.data.startswith("reset_hwid|")) @router.callback_query(F.data.startswith("reset_hwid|"))
async def handle_reset_hwid(callback_query: CallbackQuery, session: Any): async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
from panels.remnawave import RemnawaveAPI
key_name = callback_query.data.split("|")[1] key_name = callback_query.data.split("|")[1]
record = await get_key_details(session, key_name)
record_task = asyncio.create_task(get_key_details(session, key_name))
servers_task = asyncio.create_task(get_servers(session=session))
record = await record_task
if not record: if not record:
await callback_query.answer("❌ Ключ не найден.", show_alert=True) await callback_query.answer("❌ Ключ не найден.", show_alert=True)
return return
@@ -367,11 +369,8 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
await callback_query.answer("❌ У ключа отсутствует client_id.", show_alert=True) await callback_query.answer("❌ У ключа отсутствует client_id.", show_alert=True)
return return
servers = await get_servers(session=session) servers = await servers_task
remna_server = next( remna_server = next((srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"), None)
(srv for cl in servers.values() for srv in cl if srv.get("panel_type") == "remnawave"),
None,
)
if not remna_server: if not remna_server:
await callback_query.answer("❌ Remnawave-сервер не найден.", show_alert=True) await callback_query.answer("❌ Remnawave-сервер не найден.", show_alert=True)
return return
@@ -391,12 +390,12 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
deleted += 1 deleted += 1
await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True) await callback_query.answer(f"✅ Устройства сброшены ({deleted})", show_alert=True)
hook_result = await run_hooks("after_hwid_reset", chat_id=callback_query.from_user.id, admin=False, session=session, key_name=key_name) hook_result = await run_hooks(
"after_hwid_reset", chat_id=callback_query.from_user.id, admin=False, session=session, key_name=key_name
)
if hook_result and any("redirect_to_profile" in str(result) for result in hook_result): if hook_result and any("redirect_to_profile" in str(result) for result in hook_result):
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) kb.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
if callback_query.message.text: if callback_query.message.text:
await callback_query.message.edit_text("✅ Устройства сброшены", reply_markup=kb.as_markup()) await callback_query.message.edit_text("✅ Устройства сброшены", reply_markup=kb.as_markup())
else: else:
@@ -405,61 +404,3 @@ async def handle_reset_hwid(callback_query: CallbackQuery, session: Any):
image_path = os.path.join("img", "pic_view.jpg") image_path = os.path.join("img", "pic_view.jpg")
await render_key_info(callback_query.message, session, key_name, image_path) await render_key_info(callback_query.message, session, key_name, image_path)
@router.callback_query(F.data == "renew_menu")
@router.callback_query(F.data == "extend")
@router.message(F.text == "/extend")
async def process_renew_menu(callback_query_or_message: CallbackQuery | Message, session: Any):
try:
if isinstance(callback_query_or_message, CallbackQuery):
target_message = callback_query_or_message.message
tg_id = callback_query_or_message.from_user.id
else:
target_message = callback_query_or_message
tg_id = callback_query_or_message.from_user.id
records = await get_keys(session, tg_id)
servers_dict = await get_servers(session)
all_server_names = set()
for servers in servers_dict.values():
for s in servers:
all_server_names.add(s["server_name"])
builder = InlineKeyboardBuilder()
moscow_tz = pytz.timezone("Europe/Moscow")
if records:
for record in records:
if getattr(record, "is_frozen", False):
continue
alias = record.alias
email = record.email
expiry_time = record.expiry_time
server_id = record.server_id
key_display = alias.strip() if alias else email
if expiry_time:
expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz)
now = datetime.now(moscow_tz)
days_left = (expiry_date_full - now).days
if (expiry_date_full - now).total_seconds() <= 0:
days_text = "🔴 Истекла"
else:
days_text = format_days(days_left)
else:
days_text = "истекла"
server_info = f" ({server_id})" if server_id in all_server_names else ""
btn_text = f"🔑 {key_display} (⏳{days_text}) {server_info}"
builder.row(InlineKeyboardButton(text=btn_text, callback_data=f"renew_key|{email}"))
text = SELECT_SUBS
builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
image_path = os.path.join("img", "pic_view.jpg")
await edit_or_send_message(
target_message=target_message,
text=text,
reply_markup=builder.as_markup(),
media_path=image_path,
)
except Exception as e:
error_message = f"Ошибка при получении подписок для продления: {e}"
await target_message.answer(text=error_message)
+2 -2
View File
@@ -10,12 +10,12 @@ from database import get_servers, get_tariff_by_id, store_key
from database.models import User from database.models import User
from handlers.utils import check_server_key_limit from handlers.utils import check_server_key_limit
from logger import logger from logger import logger
from panels.remnawave import RemnawaveAPI from panels._3xui import (
from panels.three_xui import (
ClientConfig, ClientConfig,
add_client, add_client,
get_xui_instance, get_xui_instance,
) )
from panels.remnawave import RemnawaveAPI
async def create_key_on_cluster( async def create_key_on_cluster(
+1 -1
View File
@@ -3,8 +3,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
from database import get_servers from database import get_servers
from logger import logger from logger import logger
from panels._3xui import delete_client, get_xui_instance
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import delete_client, get_xui_instance
async def delete_key_from_cluster(cluster_id: str, email: str, client_id: str, session: AsyncSession): async def delete_key_from_cluster(cluster_id: str, email: str, client_id: str, session: AsyncSession):
+52 -17
View File
@@ -10,8 +10,8 @@ from database import delete_notification, get_servers
from database.models import Key, Server, Tariff from database.models import Key, Server, Tariff
from database.notifications import clear_hot_lead_notifications from database.notifications import clear_hot_lead_notifications
from logger import logger from logger import logger
from panels._3xui import ClientConfig, add_client, extend_client_key, get_xui_instance
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import ClientConfig, add_client, extend_client_key, get_xui_instance
async def renew_key_in_cluster( async def renew_key_in_cluster(
@@ -134,11 +134,12 @@ async def renew_key_in_cluster(
else: else:
logger.error("Не удалось войти в Remnawave API") logger.error("Не удалось войти в Remnawave API")
tasks = []
for server_info in cluster: for server_info in cluster:
if server_info.get("panel_type", "3x-ui").lower() != "3x-ui": if server_info.get("panel_type", "3x-ui").lower() != "3x-ui":
continue continue
xui = await get_xui_instance(server_info["api_url"])
inbound_id = server_info.get("inbound_id") inbound_id = server_info.get("inbound_id")
server_name = server_info.get("server_name", "unknown") server_name = server_info.get("server_name", "unknown")
@@ -155,21 +156,34 @@ async def renew_key_in_cluster(
traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0 traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
async def update_or_create_client(xui, inbound_id, unique_email, sub_id, server_name): async def process_server(server_info, inbound_id, unique_email, sub_id, server_name):
updated = await extend_client_key( try:
xui=xui, xui = await get_xui_instance(server_info["api_url"])
inbound_id=int(inbound_id), except Exception as e:
email=unique_email, logger.warning(f"[{server_name}] недоступна панель 3x-ui: {e}")
new_expiry_time=new_expiry_time, return server_name, False, f"api_unavailable: {e}"
client_id=client_id,
total_gb=traffic_bytes,
sub_id=sub_id,
tg_id=tg_id,
limit_ip=hwid_device_limit,
)
if not updated: try:
logger.warning(f"Не удалось обновить клиента {unique_email}, пробуем создать") updated = await extend_client_key(
xui=xui,
inbound_id=int(inbound_id),
email=unique_email,
new_expiry_time=new_expiry_time,
client_id=client_id,
total_gb=traffic_bytes,
sub_id=sub_id,
tg_id=tg_id,
limit_ip=hwid_device_limit,
)
except Exception as e:
logger.warning(f"[{server_name}] ошибка при продлении: {e}")
updated = False
if updated:
return server_name, True, None
logger.warning(f"[{server_name}] не удалось обновить {unique_email}, пробуем создать")
try:
config = ClientConfig( config = ClientConfig(
client_id=client_id, client_id=client_id,
email=unique_email, email=unique_email,
@@ -183,8 +197,29 @@ async def renew_key_in_cluster(
sub_id=sub_id, sub_id=sub_id,
) )
await add_client(xui, config) await add_client(xui, config)
return server_name, True, None
except Exception as e:
logger.warning(f"[{server_name}] не удалось создать клиента: {e}")
return server_name, False, f"create_failed: {e}"
tasks.append(update_or_create_client(xui, inbound_id, unique_email, sub_id, server_name)) tasks.append(process_server(server_info, inbound_id, unique_email, sub_id, server_name))
results = await asyncio.gather(*tasks, return_exceptions=True)
failed = []
succeeded = []
for r in results:
if isinstance(r, Exception):
failed.append(("unknown", f"task_exception: {r}"))
continue
name, ok, err = r
if ok:
succeeded.append(name)
else:
failed.append((name, err or "unknown_error"))
if succeeded:
logger.info(f"3x-ui продлено на: {', '.join(succeeded)}")
if failed:
logger.warning("3x-ui не продлено на: " + ", ".join([f"{n} ({e})" for n, e in failed]))
await asyncio.gather(*tasks, return_exceptions=True) await asyncio.gather(*tasks, return_exceptions=True)
+1 -1
View File
@@ -7,8 +7,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
from database import get_servers from database import get_servers
from logger import logger from logger import logger
from panels._3xui import get_xui_instance, toggle_client
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import get_xui_instance, toggle_client
async def toggle_client_on_cluster( async def toggle_client_on_cluster(
+1 -1
View File
@@ -9,8 +9,8 @@ from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
from database import get_servers from database import get_servers
from database.models import Key, Server from database.models import Key, Server
from logger import logger from logger import logger
from panels._3xui import get_client_traffic, get_xui_instance
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import get_client_traffic, get_xui_instance
async def get_user_traffic(session: AsyncSession, tg_id: int, email: str) -> dict[str, Any]: async def get_user_traffic(session: AsyncSession, tg_id: int, email: str) -> dict[str, Any]:
+1 -1
View File
@@ -10,8 +10,8 @@ from database import get_servers, store_key
from database.models import Key, Tariff from database.models import Key, Tariff
from handlers.utils import get_least_loaded_cluster from handlers.utils import get_least_loaded_cluster
from logger import logger from logger import logger
from panels._3xui import ClientConfig, add_client, get_xui_instance
from panels.remnawave import RemnawaveAPI from panels.remnawave import RemnawaveAPI
from panels.three_xui import ClientConfig, add_client, get_xui_instance
from .deletion import delete_key_from_cluster from .deletion import delete_key_from_cluster
+25 -36
View File
@@ -47,14 +47,13 @@ from handlers.texts import (
get_renewal_message, get_renewal_message,
) )
from handlers.utils import format_hours, format_minutes, get_russian_month from handlers.utils import format_hours, format_minutes, get_russian_month
from hooks.hooks import run_hooks
from logger import logger from logger import logger
from .hot_leads_notifications import notify_hot_leads from .hot_leads_notifications import notify_hot_leads
from .notify_utils import send_messages_with_limit, send_notification from .notify_utils import send_messages_with_limit, send_notification
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
from hooks.hooks import run_hooks
router = Router() router = Router()
moscow_tz = pytz.timezone("Europe/Moscow") moscow_tz = pytz.timezone("Europe/Moscow")
@@ -135,43 +134,36 @@ async def notify_24h_keys(
threshold_time_24h: int, threshold_time_24h: int,
keys: list, keys: list,
): ):
"""
Отправляет уведомления пользователям о том, что их подписка истекает через 24 часа.
"""
logger.info("Начало проверки подписок, истекающих через 24 часа.") logger.info("Начало проверки подписок, истекающих через 24 часа.")
expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_24h] expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_24h]
logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 24 часа.") logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 24 часа.")
tg_ids = [key["tg_id"] for key in expiring_keys] tg_ids = [getattr(key, "tg_id", key["tg_id"]) for key in expiring_keys]
emails = [key.email or "" for key in expiring_keys] emails = [key.email or "" for key in expiring_keys]
allowed = await check_notifications_bulk(session, "key_24h", 24, tg_ids=tg_ids, emails=emails)
users = await check_notifications_bulk(session, "key_24h", 24, tg_ids=tg_ids, emails=emails) allowed_set = {(u["tg_id"], u["email"]) for u in allowed}
messages = [] messages = []
for key in expiring_keys: for key in expiring_keys:
tg_id = key["tg_id"] tg_id = getattr(key, "tg_id", key["tg_id"])
email = key.email or "" email = key.email or ""
if (tg_id, email) not in allowed_set:
continue
notification_id = f"{email}_key_24h" notification_id = f"{email}_key_24h"
can_notify = await check_notification_time(session, tg_id, notification_id, hours=24)
if not can_notify:
continue
user = next((u for u in users if u["tg_id"] == tg_id and u["email"] == email), None)
if not user:
continue
expiry_timestamp = key.expiry_time expiry_timestamp = key.expiry_time
hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) hours_left = int((expiry_timestamp - current_time) / (1000 * 3600))
hours_left_formatted = ( hours_left_formatted = (
f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!" f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!"
) )
expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz)
formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МСК)") formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МСК)")
can_notify = await check_notification_time(session, tg_id, notification_id, hours=24)
if not can_notify:
continue
notification_text = KEY_EXPIRY_24H.format( notification_text = KEY_EXPIRY_24H.format(
email=email, email=email,
hours_left_formatted=hours_left_formatted, hours_left_formatted=hours_left_formatted,
@@ -211,10 +203,10 @@ async def notify_24h_keys(
if result: if result:
await add_notification(session, tg_id, msg["notification_id"]) await add_notification(session, tg_id, msg["notification_id"])
sent_count += 1 sent_count += 1
logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.") logger.info(f"Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
else: else:
logger.warning( logger.warning(
f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." f"Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}."
) )
logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 24 часа.") logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 24 часа.")
@@ -230,35 +222,28 @@ async def notify_10h_keys(
keys: list, keys: list,
): ):
logger.info("Начало проверки подписок, истекающих через 10 часов.") logger.info("Начало проверки подписок, истекающих через 10 часов.")
expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_10h] expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_10h]
logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 10 часов.") logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через 10 часов.")
tg_ids = [key.tg_id for key in expiring_keys] tg_ids = [key.tg_id for key in expiring_keys]
emails = [key.email or "" for key in expiring_keys] emails = [key.email or "" for key in expiring_keys]
allowed = await check_notifications_bulk(session, "key_10h", 10, tg_ids=tg_ids, emails=emails)
users = await check_notifications_bulk(session, "key_10h", 10, tg_ids=tg_ids, emails=emails) allowed_set = {(u["tg_id"], u["email"]) for u in allowed}
messages = [] messages = []
for key in expiring_keys: for key in expiring_keys:
tg_id = key.tg_id tg_id = key.tg_id
email = key.email or "" email = key.email or ""
if (tg_id, email) not in allowed_set:
continue
notification_id = f"{email}_key_10h" notification_id = f"{email}_key_10h"
can_notify = await check_notification_time(session, tg_id, notification_id, hours=10)
if not can_notify:
continue
user = next((u for u in users if u["tg_id"] == tg_id and u["email"] == email), None)
if not user:
continue
expiry_timestamp = key.expiry_time expiry_timestamp = key.expiry_time
hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) hours_left = int((expiry_timestamp - current_time) / (1000 * 3600))
hours_left_formatted = ( hours_left_formatted = (
f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!" f"⏳ Осталось времени: {format_hours(hours_left)}" if hours_left > 0 else "⏳ Последний день подписки!"
) )
expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz)
formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МСК)") formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МСК)")
@@ -268,6 +253,10 @@ async def notify_10h_keys(
formatted_expiry_date=formatted_expiry_date, formatted_expiry_date=formatted_expiry_date,
) )
can_notify = await check_notification_time(session, tg_id, notification_id, hours=10)
if not can_notify:
continue
if NOTIFY_RENEW: if NOTIFY_RENEW:
try: try:
await process_auto_renew_or_notify( await process_auto_renew_or_notify(
@@ -301,10 +290,10 @@ async def notify_10h_keys(
if result: if result:
await add_notification(session, tg_id, msg["notification_id"]) await add_notification(session, tg_id, msg["notification_id"])
sent_count += 1 sent_count += 1
logger.info(f"📢 Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.") logger.info(f"Отправлено уведомление об истекающей подписке {msg['email']} пользователю {tg_id}.")
else: else:
logger.warning( logger.warning(
f"📢 Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}." f"Не удалось отправить уведомление об истекающей подписке {msg['email']} пользователю {tg_id}."
) )
logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 10 часов.") logger.info(f"Отправлено {sent_count} уведомлений об истечении подписки через 10 часов.")
-232
View File
@@ -1,232 +0,0 @@
import os
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
FREEKASSA_ENABLE,
HELEKET_ENABLE,
KASSAI_ENABLE,
ROBOKASSA_ENABLE,
STARS_ENABLE,
WATA_INT_ENABLE,
WATA_RU_ENABLE,
WATA_SBP_ENABLE,
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
TRIBUTE_ENABLE,
TRIBUTE_LINK
)
from database import get_last_payments
from database.models import User
from handlers.buttons import (
BALANCE_HISTORY,
COUPON,
CRYPTOBOT,
FREEKASSA,
HELEKET_CRYPTO,
KASSAI_CARDS,
KASSAI_SBP,
MAIN_MENU,
PAYMENT,
ROBOKASSA,
STARS,
WATA_INT,
WATA_RU,
WATA_SBP,
YOOKASSA,
YOOMONEY,
TRIBUTE,
)
from handlers.payments.cryprobot_pay import process_callback_pay_cryptobot
from handlers.payments.freekassa_pay import process_callback_pay_freekassa
from handlers.payments.heleket import process_callback_pay_heleket
from handlers.payments.kassai import process_callback_pay_kassai
from handlers.payments.robokassa_pay import process_callback_pay_robokassa
from handlers.payments.stars_pay import process_callback_pay_stars
from handlers.payments.wata import process_callback_pay_wata
from handlers.payments.yookassa_pay import process_callback_pay_yookassa
from handlers.payments.yoomoney_pay import process_callback_pay_yoomoney
from handlers.payments.tribute_pay import process_callback_pay_tribute
from handlers.texts import BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG
from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from .utils import edit_or_send_message
router = Router()
@router.callback_query(F.data == "pay")
async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
payment_handlers = []
if YOOKASSA_ENABLE:
payment_handlers.append(process_callback_pay_yookassa)
if YOOMONEY_ENABLE:
payment_handlers.append(process_callback_pay_yoomoney)
if KASSAI_ENABLE:
payment_handlers.append(process_callback_pay_kassai)
if HELEKET_ENABLE:
payment_handlers.append(process_callback_pay_heleket)
if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE:
payment_handlers.append(process_callback_pay_wata)
if CRYPTO_BOT_ENABLE:
payment_handlers.append(process_callback_pay_cryptobot)
if STARS_ENABLE:
payment_handlers.append(process_callback_pay_stars)
if ROBOKASSA_ENABLE:
payment_handlers.append(process_callback_pay_robokassa)
if FREEKASSA_ENABLE:
payment_handlers.append(process_callback_pay_freekassa)
if TRIBUTE_ENABLE:
payment_handlers.append(process_callback_pay_tribute)
module_buttons = await run_hooks(
"pay_menu_buttons",
chat_id=callback_query.from_user.id,
admin=False,
session=session
)
has_extra_menu_items = bool(module_buttons) or bool(DONATIONS_ENABLE) or bool(TRIBUTE_ENABLE)
if len(payment_handlers) == 1 and not has_extra_menu_items:
return await payment_handlers[0](callback_query, state, session)
builder = InlineKeyboardBuilder()
if YOOKASSA_ENABLE:
builder.row(InlineKeyboardButton(text=YOOKASSA, callback_data="pay_yookassa"))
if YOOMONEY_ENABLE:
builder.row(InlineKeyboardButton(text=YOOMONEY, callback_data="pay_yoomoney"))
if KASSAI_ENABLE:
builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards"))
builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp"))
if HELEKET_ENABLE:
builder.row(InlineKeyboardButton(text=HELEKET_CRYPTO, callback_data="pay_heleket_crypto"))
if CRYPTO_BOT_ENABLE:
builder.row(InlineKeyboardButton(text=CRYPTOBOT, callback_data="pay_cryptobot"))
if STARS_ENABLE:
builder.row(InlineKeyboardButton(text=STARS, callback_data="pay_stars"))
if ROBOKASSA_ENABLE:
builder.row(InlineKeyboardButton(text=ROBOKASSA, callback_data="pay_robokassa"))
if FREEKASSA_ENABLE:
builder.row(InlineKeyboardButton(text=FREEKASSA, callback_data="pay_freekassa"))
if WATA_RU_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_RU, callback_data="pay_wata_ru"))
if WATA_SBP_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_SBP, callback_data="pay_wata_sbp"))
if WATA_INT_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_INT, callback_data="pay_wata_int"))
if TRIBUTE_ENABLE:
builder.row(InlineKeyboardButton(text=TRIBUTE, url=TRIBUTE_LINK))
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder = insert_hook_buttons(builder, module_buttons)
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text=PAYMENT_METHODS_MSG,
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "balance")
async def balance_handler(callback_query: CallbackQuery, session: AsyncSession):
stmt = select(User.balance).where(User.tg_id == callback_query.from_user.id)
result = await session.execute(stmt)
balance = result.scalar_one_or_none() or 0.0
balance = int(balance)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history"))
builder.row(InlineKeyboardButton(text=COUPON, callback_data="activate_coupon"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
text = BALANCE_MANAGEMENT_TEXT.format(balance=balance)
image_path = os.path.join("img", "pay.jpg")
await edit_or_send_message(
target_message=callback_query.message,
text=text,
reply_markup=builder.as_markup(),
media_path=image_path,
disable_web_page_preview=False,
)
@router.callback_query(F.data == "balance_history")
async def balance_history_handler(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
records = await get_last_payments(session, callback_query.from_user.id)
if records:
history_text = "<b>💳 История операций:</b>\n\n<blockquote>"
for record in records:
amount = record["amount"]
payment_system = record["payment_system"]
status = record["status"]
date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S")
history_text += f"Сумма: {amount}\nОплата: {payment_system}\nСтатус: {status}\nДата: {date}\n\n"
history_text += "</blockquote>"
else:
history_text = "❌ У вас пока нет операций с балансом."
await edit_or_send_message(
target_message=callback_query.message,
text=history_text,
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=False,
)
@router.callback_query(F.data == "pay_wata_ru")
async def handle_pay_wata_ru(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="ru")
@router.callback_query(F.data == "pay_wata_sbp")
async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="sbp")
@router.callback_query(F.data == "pay_wata_int")
async def handle_pay_wata_int(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="int")
@router.callback_query(F.data == "pay_kassai_cards")
async def handle_pay_kassai_cards(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_kassai(callback_query, state, session, method_name="cards")
@router.callback_query(F.data == "pay_kassai_sbp")
async def handle_pay_kassai_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_kassai(callback_query, state, session, method_name="sbp")
@router.callback_query(F.data == "pay_heleket_crypto")
async def handle_pay_heleket_crypto(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_heleket(callback_query, state, session, method_name="crypto")
@router.callback_query(F.data == "pay_tribute")
async def handle_pay_tribute(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_tribute(callback_query, state, session)
+29 -31
View File
@@ -2,50 +2,48 @@ __all__ = ("router",)
from aiogram import Router from aiogram import Router
from config import ( from config import PROVIDERS_ENABLED
CRYPTO_BOT_ENABLE, from handlers.payments.providers import get_providers
FREEKASSA_ENABLE,
KASSAI_ENABLE,
ROBOKASSA_ENABLE,
STARS_ENABLE,
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
HELEKET_ENABLE,
TRIBUTE_ENABLE
)
from .cryprobot_pay import router as cryprobot_router from .cryptobot import router as cryptobot_router
from .freekassa_pay import router as freekassa_router from .fast_payment_flow import router as fast_payment_flow_router
from .freekassa.freekassa_pay import router as freekassa_router
from .gift import router as gift_router from .gift import router as gift_router
from .kassai import router as kassai_router from .heleket.heleket import router as heleket_router
from .robokassa_pay import router as robokassa_router from .kassai.kassai import router as kassai_router
from .stars_pay import router as stars_router from .pay import router as pay_router
from .yookassa_pay import router as yookassa_router from .robokassa import router as robokassa_router
from .yoomoney_pay import router as yoomoney_router from .stars import router as stars_router
from .wata import router as wata_router from .tribute import router as tribute_router
from .heleket import router as heleket_router from .wata.wata import router as wata_router
from .tribute_pay import router as tribute_router from .yookassa import router as yookassa_router
from .yoomoney import router as yoomoney_router
router = Router(name="payments_main_router") router = Router(name="payments_main_router")
if YOOKASSA_ENABLE: PROVIDERS = get_providers(PROVIDERS_ENABLED)
if PROVIDERS.get("YOOKASSA", {}).get("enabled"):
router.include_router(yookassa_router) router.include_router(yookassa_router)
if YOOMONEY_ENABLE: if PROVIDERS.get("YOOMONEY", {}).get("enabled"):
router.include_router(yoomoney_router) router.include_router(yoomoney_router)
if ROBOKASSA_ENABLE: if PROVIDERS.get("ROBOKASSA", {}).get("enabled"):
router.include_router(robokassa_router) router.include_router(robokassa_router)
if FREEKASSA_ENABLE: if PROVIDERS.get("FREEKASSA", {}).get("enabled"):
router.include_router(freekassa_router) router.include_router(freekassa_router)
if CRYPTO_BOT_ENABLE: if PROVIDERS.get("CRYPTOBOT", {}).get("enabled"):
router.include_router(cryprobot_router) router.include_router(cryptobot_router)
if STARS_ENABLE: if PROVIDERS.get("STARS", {}).get("enabled"):
router.include_router(stars_router) router.include_router(stars_router)
if KASSAI_ENABLE: if PROVIDERS.get("KASSAI_CARDS", {}).get("enabled") or PROVIDERS.get("KASSAI_SBP", {}).get("enabled"):
router.include_router(kassai_router) router.include_router(kassai_router)
if HELEKET_ENABLE: if PROVIDERS.get("HELEKET", {}).get("enabled"):
router.include_router(heleket_router) router.include_router(heleket_router)
if TRIBUTE_ENABLE: if PROVIDERS.get("TRIBUTE", {}).get("enabled"):
router.include_router(tribute_router) router.include_router(tribute_router)
router.include_router(wata_router) router.include_router(wata_router)
router.include_router(gift_router) router.include_router(gift_router)
router.include_router(pay_router)
router.include_router(fast_payment_flow_router)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+76
View File
@@ -0,0 +1,76 @@
from typing import Iterable, List, Any
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.texts import FAST_PAY_NOT_ENOUGH
from handlers.buttons import RUB_CURRENCY, USD_CURRENCY, STARS, MAIN_MENU
from .currency_rates import format_for_user
def build_currency_choice_kb(show_stars: bool, *, prefix: str = "choose_payment_currency") -> InlineKeyboardBuilder:
kb = InlineKeyboardBuilder()
kb.row(InlineKeyboardButton(text=RUB_CURRENCY, callback_data=f"{prefix}|RUB"))
kb.row(InlineKeyboardButton(text=USD_CURRENCY, callback_data=f"{prefix}|USD"))
if show_stars:
kb.row(InlineKeyboardButton(text=STARS, callback_data=f"{prefix}|STARS"))
kb.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
return kb
async def shortfall_lead_text(
session: Any,
tg_id: int,
required_amount: int | float | None,
language_code: str | None,
*,
force_currency: str | None = None,
) -> str:
if not isinstance(required_amount, (int, float)) or required_amount <= 0:
return "💳"
amount_txt = await format_for_user(
session, tg_id, float(required_amount), language_code, force_currency=force_currency
)
return FAST_PAY_NOT_ENOUGH.format(amount=amount_txt)
def filter_providers_by_currency(
currency: str,
providers: Iterable[str],
rub_providers: Iterable[str],
) -> List[str]:
rub_set = {p.upper() for p in rub_providers}
out: List[str] = []
for p in providers:
up = p.upper()
if currency == "RUB":
if up in rub_set or up == "WATA":
out.append(p)
elif currency == "USD":
if (up not in rub_set or up == "WATA") and up != "STARS":
out.append(p)
elif currency == "STARS":
if up == "STARS":
out.append(p)
else:
out.append(p)
return out
def currency_for_provider(up_provider: str, rub_providers: Iterable[str]) -> str | None:
if up_provider in {p.upper() for p in rub_providers}:
return "RUB"
if up_provider == "STARS":
return "STARS"
if up_provider == "WATA":
return None
return "USD"
def currency_label(code: str) -> str:
if code == "RUB":
return "RUB"
if code == "USD":
return "USD/Cryptowallet"
if code == "STARS":
return "Telegram Stars"
return code
+187
View File
@@ -0,0 +1,187 @@
from __future__ import annotations
import sqlalchemy as sa
from typing import Optional, Tuple
import time
from decimal import ROUND_HALF_UP, Decimal
import aiohttp
from config import MULTICURRENCY_ENABLE, FX_MARKUP
CBR_URL = "https://www.cbr-xml-daily.ru/daily_json.js"
CACHE_TTL = 60 * 30
cache: dict[str, tuple[float, Decimal]] = {}
def _q(x: Decimal, prec: int = 8) -> Decimal:
return x.quantize(Decimal(10) ** -prec, rounding=ROUND_HALF_UP)
def _round2(x: Decimal) -> Decimal:
return x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
async def to_rub(amount: float | Decimal, base: str, *, session: aiohttp.ClientSession | None = None) -> Decimal:
"""
Переводит сумму ИЗ валюты base В РУБЛИ.
Использует get_rub_rate(base): base_per_rub, т.е. СКОЛЬКО единиц base в 1 рубле.
RUB = amount / (base_per_rub).
"""
rate = await get_rub_rate(base, session=session)
return _q(Decimal(amount) / rate, prec=2)
async def get_rub_rate(quote: str, *, session: aiohttp.ClientSession | None = None) -> Decimal:
"""
Возвращает курс 'QUOTE per RUB' (сколько единиц валюты QUOTE приходится на 1 рубль).
Пример: для USD при 100 RUB за 1 USD вернёт 0.01 USD/RUB.
Здесь же применяется наценка FX_MARKUP (если задана и валюта не RUB).
"""
code = quote.upper()
if code == "RUB":
return Decimal("1")
now = time.time()
cached = cache.get(code)
if cached and now - cached[0] < CACHE_TTL:
return cached[1]
owns = False
s = session
if s is None:
s = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
owns = True
try:
async with s.get(CBR_URL, headers={"Accept": "application/json"}) as resp:
resp.raise_for_status()
data = await resp.json(content_type=None)
finally:
if owns and not s.closed:
await s.close()
valutes = data.get("Valute") or {}
v = valutes.get(code)
if not v:
raise ValueError(f"Валюта {code} не найдена у ЦБ")
rub_per_unit = Decimal(str(v["Value"])) / Decimal(str(v.get("Nominal", 1)))
rate = _q(Decimal("1") / rub_per_unit)
if code != "RUB" and FX_MARKUP:
rate = _q(rate * (Decimal("1") + Decimal(str(FX_MARKUP))))
cache[code] = (now, rate)
return rate
async def convert_from_rub(
amount_rub: Decimal | float,
to_ccy: str,
*,
session: aiohttp.ClientSession | None = None
) -> Decimal:
"""
Конвертирует сумму из RUB в валюту to_ccy, используя get_rub_rate(to_ccy).
"""
amt = Decimal(str(amount_rub))
ccy = to_ccy.upper()
if ccy == "RUB":
return _round2(amt)
rate = await get_rub_rate(ccy, session=session)
val = amt * rate
return _round2(val)
def pick_currency(
language_code: str | None,
user_currency: str | None = None,
force_currency: str | None = None
) -> str:
if not MULTICURRENCY_ENABLE:
return "RUB"
if force_currency in {"USD", "RUB"}:
return force_currency
if user_currency in {"USD", "RUB"}:
return user_currency
code = (language_code or "").split("-")[0].lower()
return "RUB" if code == "ru" else "USD"
def fmt_money(amount: Decimal, currency: str, language_code: str | None) -> str:
q = _round2(amount)
if currency == "USD":
s = f"{q:,.2f}"
if (language_code or "").startswith("ru"):
s = s.replace(",", " ")
return f"${s}"
s = f"{q:,.2f}".replace(",", " ")
return f"{s}"
async def display_price(
amount_rub: Decimal | float,
language_code: str | None,
*,
user_currency: str | None = None,
force_currency: str | None = None,
session: aiohttp.ClientSession | None = None,
) -> tuple[str, str, Decimal]:
if force_currency in {"USD", "RUB"}:
cur = force_currency
if cur == "RUB":
val = _round2(Decimal(str(amount_rub)))
else:
val = await convert_from_rub(Decimal(str(amount_rub)), "USD", session=session)
txt = fmt_money(val, cur, language_code)
return txt, cur, val
val = _round2(Decimal(str(amount_rub)))
txt = fmt_money(val, "RUB", language_code)
return txt, "RUB", val
async def money_for_user(
db_session,
tg_id: int,
amount_rub: float | int | Decimal,
language_code: Optional[str],
force_currency: Optional[str] = None,
) -> Tuple[str, str, Decimal]:
"""
Возвращает: (text, currency, value)
- text: строка для показа пользователю, например "$12.34" или "1 234.00 ₽"
- currency: "USD" или "RUB"
- value: Decimal в выбранной валюте
"""
row = await db_session.execute(
sa.text("select preferred_currency from users where tg_id = :id"),
{"id": tg_id},
)
user_currency = row.scalar()
txt, cur, val = await display_price(
amount_rub,
language_code,
user_currency=user_currency,
force_currency=force_currency,
session=None,
)
return txt, cur, val
async def format_for_user(
db_session,
tg_id: int,
amount_rub: float | int | Decimal,
language_code: Optional[str],
force_currency: Optional[str] = None,
) -> str:
text, _, _ = await money_for_user(
db_session,
tg_id,
amount_rub,
language_code,
force_currency=force_currency,
)
return text
+217
View File
@@ -0,0 +1,217 @@
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import USE_NEW_PAYMENT_FLOW, MULTICURRENCY_ENABLE, PROVIDERS_ENABLED
from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, FAST_PAY_CHOOSE_PROVIDER
from database.temporary_data import create_temporary_data
from handlers import buttons as btn
from handlers.utils import edit_or_send_message
from logger import logger
from handlers.payments.currency_flow import (
build_currency_choice_kb,
shortfall_lead_text,
currency_label,
)
from handlers.payments.providers import get_providers_with_hooks
router = Router()
async def _run_provider_flow(
provider: str,
callback_query: CallbackQuery,
session: Any,
state: FSMContext,
required_amount: int | None,
) -> bool:
import importlib
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
up = provider.upper()
cfg = (PROVIDERS.get(up) or {})
fast_name = cfg.get("fast")
if not fast_name:
return False
module_name = f"handlers.payments.{up.lower()}.handlers"
try:
module = importlib.import_module(module_name)
func = getattr(module, fast_name)
except Exception as e:
logger.error(f"[FAST_FLOW] Импорт {up}.{fast_name} из {module_name} не удался: {e}")
return False
try:
if up == "STARS":
try:
await callback_query.message.delete()
except Exception as e:
logger.warning(f"[FAST_FLOW] Не удалось удалить меню перед STARS: {e}")
await func(callback_query, session)
return True
except Exception as e:
logger.error(f"[FAST_FLOW] Ошибка при вызове {up}.{fast_name}(): {e}")
return False
async def try_fast_payment_flow(
callback_query: CallbackQuery,
session: Any,
state: FSMContext,
*,
tg_id: int,
temp_key: str,
temp_payload: dict,
required_amount: int | None = None,
) -> bool:
await create_temporary_data(session, tg_id, temp_key, temp_payload)
if not USE_NEW_PAYMENT_FLOW:
return False
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
providers = (
[USE_NEW_PAYMENT_FLOW] if isinstance(USE_NEW_PAYMENT_FLOW, str)
else [str(p) for p in (USE_NEW_PAYMENT_FLOW or [])]
)
providers = [
p for p in providers
if (PROVIDERS.get(str(p).upper()) or {}).get("fast")
and (PROVIDERS.get(str(p).upper()) or {}).get("enabled", True)
]
if not providers:
return False
if len(providers) == 1:
up = providers[0].upper()
cfg = PROVIDERS.get(up) or {}
currency = cfg.get("currency")
if currency:
await state.update_data(chosen_currency=currency)
if await _run_provider_flow(up, callback_query, session, state, required_amount):
return True
return False
if MULTICURRENCY_ENABLE:
show_stars = bool((PROVIDERS.get("STARS") or {}).get("enabled"))
kb = build_currency_choice_kb(show_stars=show_stars)
lead = await shortfall_lead_text(
session, tg_id, required_amount, getattr(callback_query.from_user, "language_code", None)
)
text = f"{lead}.\n\n{FAST_PAY_CHOOSE_CURRENCY}"
await state.update_data(
temp_key=temp_key,
temp_payload=temp_payload,
required_amount=required_amount,
fastflow_providers=providers,
)
await edit_or_send_message(target_message=callback_query.message, text=text, reply_markup=kb.as_markup())
return True
kb = InlineKeyboardBuilder()
for p in providers:
up = p.upper()
btn_text = getattr(btn, up, up)
kb.row(InlineKeyboardButton(text=btn_text, callback_data=f"choose_payment_provider|{up}"))
kb.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
lead = await shortfall_lead_text(
session, tg_id, required_amount, getattr(callback_query.from_user, "language_code", None)
)
await state.update_data(temp_key=temp_key, temp_payload=temp_payload, required_amount=required_amount)
await edit_or_send_message(
target_message=callback_query.message,
text=f"{lead}.\n\n{FAST_PAY_CHOOSE_PROVIDER}",
reply_markup=kb.as_markup(),
)
return True
@router.callback_query(F.data.startswith("choose_payment_currency|"))
async def choose_payment_currency(callback_query: CallbackQuery, state: FSMContext, session: Any):
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
currency = callback_query.data.split("|")[1]
data = await state.get_data()
providers = data.get("fastflow_providers") or (
[USE_NEW_PAYMENT_FLOW] if isinstance(USE_NEW_PAYMENT_FLOW, str)
else [str(p) for p in (USE_NEW_PAYMENT_FLOW or [])]
)
filtered = [
p.upper() for p in providers
if (PROVIDERS.get(str(p).upper()) or {}).get("currency") == currency
and (PROVIDERS.get(str(p).upper()) or {}).get("fast")
and (PROVIDERS.get(str(p).upper()) or {}).get("enabled", True)
]
await state.update_data(chosen_currency=currency)
if not filtered:
kb = InlineKeyboardBuilder().row(InlineKeyboardButton(text="← Назад", callback_data="profile"))
await edit_or_send_message(
target_message=callback_query.message,
text="Для выбранной валюты нет доступных касс. Выберите другую валюту или вернитесь в меню.",
reply_markup=kb.as_markup(),
)
return
if len(filtered) == 1:
only = filtered[0]
if await _run_provider_flow(only, callback_query, session, state, data.get("required_amount")):
return
await edit_or_send_message(
target_message=callback_query.message,
text="Этот способ временно недоступен.",
reply_markup=InlineKeyboardBuilder().row(
InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile")
).as_markup(),
)
return
kb = InlineKeyboardBuilder()
for p in filtered:
btn_text = getattr(btn, p, p)
kb.row(InlineKeyboardButton(text=btn_text, callback_data=f"choose_payment_provider|{p}"))
kb.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
lead = await shortfall_lead_text(
session,
callback_query.from_user.id,
data.get("required_amount"),
getattr(callback_query.from_user, "language_code", None),
force_currency=currency,
)
text = f"{lead}.\n\nВалюта: {currency_label(currency)}\n{FAST_PAY_CHOOSE_PROVIDER}"
await edit_or_send_message(target_message=callback_query.message, text=text, reply_markup=kb.as_markup())
@router.callback_query(F.data.startswith("choose_payment_provider|"))
async def choose_payment_provider(callback_query: CallbackQuery, state: FSMContext, session: Any):
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
provider = callback_query.data.split("|")[1].upper()
cfg = PROVIDERS.get(provider) or {}
if not cfg.get("fast") or not cfg.get("enabled", True):
await edit_or_send_message(
target_message=callback_query.message,
text="Этот способ временно недоступен.",
reply_markup=InlineKeyboardBuilder().row(
InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile")
).as_markup(),
)
return
currency = cfg.get("currency")
if currency:
await state.update_data(chosen_currency=currency)
data = await state.get_data()
await _run_provider_flow(provider, callback_query, session, state, data.get("required_amount"))
@@ -1,405 +1,365 @@
import hashlib import hashlib
from typing import Any
from datetime import datetime, timedelta
from aiogram import F, Router, types from typing import Any
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram import F, Router, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.fsm.state import State, StatesGroup
from aiohttp import web from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from robokassa import HashAlgorithm, Robokassa from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession from aiohttp import web
from datetime import datetime, timedelta from sqlalchemy import and_, select
from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession
from pytz import timezone
from urllib.parse import urlencode, quote_plus from config import (
from config import ( FREEKASSA_SECRET1,
ROBOKASSA_ENABLE, FREEKASSA_SECRET2,
ROBOKASSA_LOGIN, FREEKASSA_SHOP_ID,
ROBOKASSA_PASSWORD1, )
ROBOKASSA_PASSWORD2, from database import (
ROBOKASSA_TEST_MODE, add_payment,
) add_user,
from database import ( async_session_maker,
add_payment, check_user_exists,
add_user, get_key_count,
async_session_maker, get_temporary_data,
check_user_exists, update_balance,
get_key_count, )
get_temporary_data, from database.models import Payment
update_balance, from handlers.buttons import BACK, PAY_2
Payment from handlers.payments.utils import send_payment_success_notification
) from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
import json from handlers.utils import edit_or_send_message
from decimal import Decimal, ROUND_DOWN from logger import logger
from handlers.buttons import BACK, PAY_2
from handlers.payments.utils import send_payment_success_notification
from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS router = Router()
from handlers.utils import edit_or_send_message
from logger import logger
class ReplenishBalanceState(StatesGroup):
router = Router() choosing_amount_freekassa = State()
waiting_for_payment_confirmation_freekassa = State()
MOSCOW_TZ = timezone("Europe/Moscow")
def generate_signature(shop_id: int, amount: float, secret: str, order_id: str, currency: str = "RUB") -> str:
class ReplenishBalanceState(StatesGroup): signature_string = f"{shop_id}:{amount}:{secret}:{currency}:{order_id}"
choosing_amount_robokassa = State() signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
waiting_for_payment_confirmation_robokassa = State() logger.debug(f"Generated signature for order {order_id}: {signature}")
return signature
if ROBOKASSA_ENABLE:
robokassa = Robokassa( def generate_payment_link(amount: float, order_id: str, tg_id: int, currency: str = "RUB") -> str:
merchant_login=ROBOKASSA_LOGIN, signature = generate_signature(FREEKASSA_SHOP_ID, amount, FREEKASSA_SECRET1, order_id, currency)
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2, payment_url = "https://pay.fk.money/"
algorithm=HashAlgorithm.md5, params = {
is_test=ROBOKASSA_TEST_MODE, "m": FREEKASSA_SHOP_ID,
) "oa": amount,
"currency": currency,
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN) "o": order_id,
"s": signature,
"us_tg_id": tg_id,
def _build_receipt(amount: float, *, sno: str = "usn_income") -> dict: }
receipt = {
"items": [{ query_string = "&".join([f"{key}={value}" for key, value in params.items()])
"name": "Пополнение баланса", full_url = f"{payment_url}?{query_string}"
"quantity": 1,
"sum": float(amount), logger.info(f"Generated Freekassa payment link: {full_url}")
"payment_method": "full_payment", return full_url
"payment_object": "payment",
"tax": "none",
}], @router.callback_query(F.data == "pay_freekassa")
"sno": sno, async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
} tg_id = callback_query.message.chat.id
return receipt logger.info(f"User {tg_id} initiated Freekassa payment.")
builder = InlineKeyboardBuilder()
def _format_amount(amount: float | int) -> str: for i in range(0, len(PAYMENT_OPTIONS), 2):
s = str(Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_DOWN)) if i + 1 < len(PAYMENT_OPTIONS):
if "." in s: builder.row(
s = s.rstrip("0").rstrip(".") InlineKeyboardButton(
return s text=PAYMENT_OPTIONS[i]["text"],
callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
def generate_payment_link(amount, inv_id, description, tg_id): ),
out_sum = _format_amount(amount) InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
receipt = _build_receipt(amount) callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
receipt_json = json.dumps(receipt, ensure_ascii=False, separators=(",", ":")) ),
)
receipt_enc1 = quote_plus(receipt_json, safe="") else:
builder.row(
shp = {"Shp_id": str(tg_id)} InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
base = f"{ROBOKASSA_LOGIN}:{out_sum}:{inv_id}:{receipt_enc1}:{ROBOKASSA_PASSWORD1}" callback_data=f"freekassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}",
for k in sorted(shp.keys(), key=str.lower): )
base += f":{k}={shp[k]}" )
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
signature = hashlib.md5(base.encode("utf-8")).hexdigest().upper()
key_count = await get_key_count(session, tg_id)
query = {
"MrchLogin": ROBOKASSA_LOGIN, if key_count == 0:
"OutSum": out_sum, exists = await check_user_exists(session, tg_id)
"InvId": inv_id, if not exists:
"Description": description, from_user = callback_query.from_user
"Receipt": receipt_enc1, await add_user(
"SignatureValue": signature, tg_id=from_user.id,
**shp, username=from_user.username,
} first_name=from_user.first_name,
if ROBOKASSA_TEST_MODE: last_name=from_user.last_name,
query["IsTest"] = 1 language_code=from_user.language_code,
is_bot=from_user.is_bot,
return "https://auth.robokassa.ru/Merchant/Index.aspx?" + urlencode(query) session=session,
)
logger.info(f"[DB] Новый пользователь {tg_id} создан через Freekassa.")
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa( await callback_query.message.delete()
callback_query: types.CallbackQuery, state: FSMContext, session: Any
): new_message = await callback_query.message.answer(
tg_id = callback_query.message.chat.id text="Выберите сумму пополнения:",
logger.info(f"User {tg_id} initiated Robokassa payment.") reply_markup=builder.as_markup(),
)
builder = InlineKeyboardBuilder() await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id)
for i in range(0, len(PAYMENT_OPTIONS), 2): await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
if i + 1 < len(PAYMENT_OPTIONS): logger.info(f"Displayed amount selection for user {tg_id}.")
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], @router.callback_query(F.data.startswith("freekassa_amount|"))
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
), logger.info(f"Получены данные callback_data: {callback_query.data}")
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], data = callback_query.data.split("|")
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', if len(data) != 3 or data[1] != "amount":
), logger.error("Ошибка: callback_data не соответствует формату.")
) await edit_or_send_message(
else: target_message=callback_query.message,
builder.row( text="Ошибка: данные повреждены.",
InlineKeyboardButton( reply_markup=types.InlineKeyboardMarkup(),
text=PAYMENT_OPTIONS[i]["text"], force_text=True,
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', )
) return
)
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) amount_str = data[2]
try:
key_count = await get_key_count(session, tg_id) amount = float(amount_str)
if amount <= 0:
if key_count == 0: raise ValueError("Сумма должна быть положительным числом.")
exists = await check_user_exists(session, tg_id) except ValueError as e:
if not exists: logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
from_user = callback_query.from_user await edit_or_send_message(
await add_user( target_message=callback_query.message,
tg_id=from_user.id, text="Некорректная сумма.",
username=from_user.username, reply_markup=types.InlineKeyboardMarkup(),
first_name=from_user.first_name, force_text=True,
last_name=from_user.last_name, )
language_code=from_user.language_code, return
is_bot=from_user.is_bot,
session=session, await state.update_data(amount=amount)
) logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.")
logger.info(f"[DB] Новый пользователь {tg_id} создан через Robokassa.")
tg_id = callback_query.message.chat.id
await callback_query.message.delete() order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
new_message = await callback_query.message.answer( payment_url = generate_payment_link(amount, order_id, tg_id)
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(), logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}")
)
await state.update_data( confirm_keyboard = InlineKeyboardMarkup(
message_id=new_message.message_id, chat_id=new_message.chat.id inline_keyboard=[
) [InlineKeyboardButton(text=PAY_2, url=payment_url)],
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) [InlineKeyboardButton(text=BACK, callback_data="pay_freekassa")],
logger.info(f"Displayed amount selection for user {tg_id}.") ]
)
@router.callback_query(F.data.startswith("robokassa_amount|")) await edit_or_send_message(
async def process_amount_selection( target_message=callback_query.message,
callback_query: types.CallbackQuery, state: FSMContext text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount),
): reply_markup=confirm_keyboard,
logger.info(f"Получены данные callback_data: {callback_query.data}") force_text=True,
)
data = callback_query.data.split("|") logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
if len(data) != 3 or data[1] != "amount":
logger.error("Ошибка: callback_data не соответствует формату.")
await edit_or_send_message( def verify_signature(params: dict) -> bool:
target_message=callback_query.message, try:
text="Ошибка: данные повреждены.", merchant_id = params.get("MERCHANT_ID", "")
reply_markup=types.InlineKeyboardMarkup(), amount = params.get("AMOUNT", "")
force_text=True, merchant_order_id = params.get("MERCHANT_ORDER_ID", "")
) sign = params.get("SIGN", "")
return
signature_string = f"{merchant_id}:{amount}:{FREEKASSA_SECRET2}:{merchant_order_id}"
amount_str = data[2] expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest()
try:
amount = int(amount_str) logger.debug(f"Signature verification: expected={expected_signature}, received={sign}")
if amount <= 0:
raise ValueError("Сумма должна быть положительным числом.") return expected_signature == sign
except ValueError as e: except Exception as e:
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}") logger.error(f"Error verifying signature: {e}")
await edit_or_send_message( return False
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=types.InlineKeyboardMarkup(), async def freekassa_webhook(request: web.Request):
force_text=True, try:
) params = dict(request.query)
return logger.info(f"Received Freekassa webhook: {params}")
await state.update_data(amount=amount) merchant_id = params.get("MERCHANT_ID")
logger.info(f"User {callback_query.message.chat.id} selected amount: {amount}.") amount = params.get("AMOUNT")
inv_id = 0 merchant_order_id = params.get("MERCHANT_ORDER_ID")
sign = params.get("SIGN")
tg_id = callback_query.message.chat.id tg_id = params.get("us_tg_id")
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
if not all([merchant_id, amount, merchant_order_id, sign]):
logger.info(f"Payment URL for user {callback_query.message.chat.id}: {payment_url}") logger.error("Missing required parameters in webhook")
return web.Response(status=400, text="Missing required parameters")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ if not verify_signature(params):
[InlineKeyboardButton(text=PAY_2, url=payment_url)], logger.error("Invalid signature in webhook")
[InlineKeyboardButton(text=BACK, callback_data="pay_robokassa")], return web.Response(status=400, text="Invalid signature")
]
) if str(merchant_id) != str(FREEKASSA_SHOP_ID):
logger.error(f"Invalid merchant_id: {merchant_id}")
await edit_or_send_message( return web.Response(status=400, text="Invalid merchant_id")
target_message=callback_query.message,
text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount), try:
reply_markup=confirm_keyboard, amount_float = float(amount)
force_text=True, if tg_id:
) tg_id_int = int(tg_id)
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.") else:
order_parts = merchant_order_id.split("_")
if len(order_parts) >= 3 and order_parts[0] == "order":
async def robokassa_webhook(request: web.Request): tg_id_int = int(order_parts[1])
try: else:
params = await request.post() logger.error(f"Cannot extract tg_id from order_id: {merchant_order_id}")
return web.Response(status=400, text="Cannot identify user")
logger.info(f"Received webhook params: {params}") except (ValueError, TypeError) as e:
logger.error(f"Error parsing parameters: {e}")
amount = params.get("OutSum") return web.Response(status=400, text="Invalid parameter format")
inv_id = params.get("InvId")
shp_id = params.get("Shp_id") or params.get("shp_id") or params.get("id") async with async_session_maker() as session:
signature_value = params.get("SignatureValue") recent_time = datetime.utcnow() - timedelta(minutes=1)
result = await session.execute(
logger.info( select(Payment).where(
f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}" and_(
) Payment.tg_id == tg_id_int,
Payment.amount == amount_float,
if not check_payment_signature(params): Payment.status == "success",
logger.error("Неверная подпись или данные запроса.") Payment.created_at >= recent_time,
return web.Response(status=400) )
)
if not amount or not inv_id or not shp_id: )
logger.error("Отсутствуют обязательные параметры.") duplicate = result.scalar_one_or_none()
return web.Response(status=400)
if duplicate:
tg_id = shp_id logger.warning(
logger.info(f"Processing payment for user {tg_id} with amount {amount}.") f"[Freekassa] Повторный webhook. Платёж уже обработан: tg_id={tg_id_int}, amount={amount_float}"
)
async with async_session_maker() as session: return web.Response(text="YES")
recent_time = datetime.now(MOSCOW_TZ).replace(tzinfo=None) - timedelta(seconds=10)
await update_balance(session, tg_id_int, amount_float)
result = await session.execute( await send_payment_success_notification(tg_id_int, amount_float, session)
select(Payment).where( await add_payment(session, tg_id_int, amount_float, "freekassa")
and_(
Payment.tg_id == int(tg_id), logger.info(f"Payment processed successfully. User: {tg_id_int}, Amount: {amount_float}")
Payment.amount == float(amount), return web.Response(text="YES")
Payment.status == "success",
Payment.created_at >= recent_time except Exception as e:
) logger.error(f"Error processing Freekassa webhook: {e}")
) return web.Response(status=500, text="Internal server error")
)
duplicate = result.scalar_one_or_none()
@router.callback_query(F.data == "enter_custom_amount_freekassa")
if duplicate: async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
logger.warning(f"[Robokassa] Повторный webhook. Платёж уже обработан: tg_id={tg_id}, amount={amount}") tg_id = callback_query.message.chat.id
return web.Response(text=f"OK{inv_id}") logger.info(f"User {tg_id} chose to enter a custom amount.")
await update_balance(session, int(tg_id), float(amount)) builder = InlineKeyboardBuilder()
await send_payment_success_notification(tg_id, float(amount), session) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
await add_payment(session, int(tg_id), float(amount), "robokassa")
await edit_or_send_message(
logger.info(f"✅ Payment successful. Balance updated for user {tg_id}.") target_message=callback_query.message,
return web.Response(text=f"OK{inv_id}") text=ENTER_SUM,
reply_markup=builder.as_markup(),
except Exception as e: force_text=True,
logger.error(f"Error processing webhook: {e}") )
return web.Response(status=500)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
def check_payment_signature(params):
"""Проверка подписи ResultURL от Robokassa с учётом всех Shp_*.""" @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_freekassa)
out_sum = params.get("OutSum") or params.get("out_summ") or params.get("outsumm") async def handle_custom_amount_input(
inv_id = params.get("InvId") or params.get("inv_id") or params.get("invid") message: types.Message | types.CallbackQuery,
received_sig = (params.get("SignatureValue") or params.get("signaturevalue") or "").upper() state: FSMContext = None,
session: AsyncSession = None,
if not out_sum or not inv_id or not received_sig: ):
logger.error("Missing required params for signature check.") if isinstance(message, types.CallbackQuery):
return False tg_id = message.message.chat.id
target_message = message.message
shp_items = [(k, params[k]) for k in params.keys() if k.lower().startswith("shp_")] else:
shp_items.sort(key=lambda kv: kv[0].lower()) tg_id = message.chat.id
shp_suffix = "".join(f":{k}={v}" for k, v in shp_items) target_message = message
base = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}{shp_suffix}"
expected_sig = hashlib.md5(base.encode("utf-8")).hexdigest().upper() logger.info(f"User {tg_id} initiated payment through Freekassa")
logger.info(f"Signature base (RESULT): {base}") try:
logger.info(f"Expected signature: {expected_sig}") user_data = await get_temporary_data(session, tg_id)
logger.info(f"Received signature: {received_sig}")
if not user_data:
return received_sig == expected_sig await edit_or_send_message(
target_message=target_message,
text="Данные для оплаты не найдены. Попробуйте снова.",
@router.callback_query(F.data == "enter_custom_amount_robokassa") reply_markup=types.InlineKeyboardMarkup(),
async def process_custom_amount_selection( )
callback_query: types.CallbackQuery, state: FSMContext return
):
tg_id = callback_query.message.chat.id state_type = user_data["state"]
logger.info(f"User {tg_id} chose to enter a custom amount.") amount = user_data["data"].get("required_amount", 0)
builder = InlineKeyboardBuilder() if amount <= 0:
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_robokassa")) await edit_or_send_message(
target_message=target_message,
await edit_or_send_message( text="Недостаточная сумма для пополнения.",
target_message=callback_query.message, reply_markup=types.InlineKeyboardMarkup(),
text=ENTER_SUM, )
reply_markup=builder.as_markup(), return
force_text=True,
) order_id = f"order_{tg_id}_{int(amount)}_{hash(str(tg_id) + str(amount))}"
payment_url = generate_payment_link(amount, order_id, tg_id)
await state.set_state( logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
) builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_freekassa"))
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input( if state_type == "waiting_for_payment":
message: types.Message | types.CallbackQuery, message_text = (
state: FSMContext = None, f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
session: AsyncSession = None, )
): elif state_type == "waiting_for_renewal_payment":
if isinstance(message, types.CallbackQuery): message_text = (
tg_id = message.message.chat.id f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
target_message = message.message )
else: else:
tg_id = message.chat.id await edit_or_send_message(
target_message = message target_message=target_message,
text="Некорректное состояние данных. Попробуйте снова.",
logger.info(f"User {tg_id} initiated payment through ROBOKASSA") reply_markup=types.InlineKeyboardMarkup(),
inv_id = 0 )
return
try:
user_data = await get_temporary_data(session, tg_id) await edit_or_send_message(
target_message=target_message,
if not user_data: text=message_text,
await edit_or_send_message( reply_markup=builder.as_markup(),
target_message=target_message, )
text="Данные для оплаты не найдены. Попробуйте снова.",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[]), if isinstance(state, FSMContext):
) await state.clear()
return
except Exception as e:
state_type = user_data["state"] logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
amount = user_data["data"].get("required_amount", 0) await edit_or_send_message(
target_message=target_message,
if amount <= 0: text="Произошла ошибка при создании платежа. Попробуйте позже.",
await edit_or_send_message( reply_markup=types.InlineKeyboardMarkup(),
target_message=target_message, )
text="Недостаточная сумма для пополнения.",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[]),
)
return
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_robokassa"))
if state_type == "waiting_for_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
elif state_type == "waiting_for_renewal_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
else:
await edit_or_send_message(
target_message=target_message,
text="Некорректное состояние данных. Попробуйте снова.",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[]),
)
return
await edit_or_send_message(
target_message=target_message,
text=message_text,
reply_markup=builder.as_markup(),
)
if isinstance(state, FSMContext):
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
await edit_or_send_message(
target_message=target_message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[]),
)
@@ -1,8 +1,10 @@
import aiohttp
import hashlib
import base64 import base64
import time import hashlib
import json import json
import time
from decimal import Decimal, ROUND_HALF_UP
import aiohttp
from aiogram import F, Router, types from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
@@ -11,18 +13,22 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import ( from config import (
HELEKET_ENABLE, HELEKET_MERCHANT_ID, HELEKET_API_KEY, HELEKET_API_KEY,
HELEKET_SUCCESS_URL, HELEKET_RETURN_URL, HELEKET_CALLBACK_URL, HELEKET_CALLBACK_URL,
HELEKET_CURRENCY_RATE HELEKET_MERCHANT_ID,
) HELEKET_RETURN_URL,
HELEKET_SUCCESS_URL,
from handlers.buttons import BACK, PAY_2, HELEKET_CRYPTO PROVIDERS_ENABLED,
from handlers.texts import (
HELEKET_CRYPTO_DESCRIPTION, HELEKET_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
) )
from handlers.payments.providers import get_providers
from ..currency_rates import get_rub_rate
from handlers.buttons import BACK, HELEKET, PAY_2
from handlers.texts import ENTER_SUM, HELEKET_CRYPTO_DESCRIPTION, HELEKET_PAYMENT_MESSAGE, PAYMENT_OPTIONS
from handlers.utils import edit_or_send_message from handlers.utils import edit_or_send_message
from database import add_payment, async_session_maker
from logger import logger from logger import logger
router = Router() router = Router()
@@ -33,23 +39,35 @@ class ReplenishBalanceHeleket(StatesGroup):
entering_custom_amount = State() entering_custom_amount = State()
PROVIDERS = get_providers(PROVIDERS_ENABLED)
HELEKET_PAYMENT_METHODS = [ HELEKET_PAYMENT_METHODS = [
{"enable": HELEKET_ENABLE, "currency": "USD", "to_currency": None, "name": "crypto", "button": HELEKET_CRYPTO, "desc": HELEKET_CRYPTO_DESCRIPTION}, {
"enable": bool(PROVIDERS.get("HELEKET", {}).get("enabled")),
"currency": (PROVIDERS.get("HELEKET", {}).get("currency") or "USD"),
"to_currency": None,
"name": "crypto",
"button": HELEKET,
"desc": HELEKET_CRYPTO_DESCRIPTION,
},
] ]
@router.callback_query(F.data == "pay_heleket") @router.callback_query(F.data == "pay_heleket_crypto")
async def process_callback_pay_heleket(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None): async def process_callback_pay_heleket(
callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None
):
try: try:
tg_id = callback_query.message.chat.id tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated Heleket payment.") logger.info(f"User {tg_id} initiated Heleket payment.")
await state.clear() await state.clear()
if method_name: if method_name:
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None) method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
if not method: if not method:
await callback_query.message.delete() try:
await callback_query.message.delete()
except Exception:
pass
await callback_query.message.answer( await callback_query.message.answer(
text="Ошибка: выбранный способ оплаты недоступен.", text="Ошибка: выбранный способ оплаты недоступен.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]), reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
@@ -62,24 +80,27 @@ async def process_callback_pay_heleket(callback_query: types.CallbackQuery, stat
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
) )
) )
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}")) builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
await callback_query.message.delete() try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer( new_msg = await callback_query.message.answer(
text=method["desc"], text=method["desc"],
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
@@ -96,10 +117,15 @@ async def process_callback_pay_heleket(callback_query: types.CallbackQuery, stat
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for method in HELEKET_PAYMENT_METHODS: for method in HELEKET_PAYMENT_METHODS:
if method["enable"]: if method["enable"]:
builder.row(InlineKeyboardButton(text=method["button"], callback_data=f'heleket_method|{method["name"]}')) builder.row(
InlineKeyboardButton(text=method["button"], callback_data=f"heleket_method|{method['name']}")
)
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
await callback_query.message.delete() try:
await callback_query.message.delete()
except Exception:
pass
new_msg = await callback_query.message.answer( new_msg = await callback_query.message.answer(
text="Выберите способ оплаты через Heleket:", text="Выберите способ оплаты через Heleket:",
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
@@ -116,7 +142,7 @@ async def process_callback_pay_heleket(callback_query: types.CallbackQuery, stat
async def process_method_selection(callback_query: types.CallbackQuery, state: FSMContext): async def process_method_selection(callback_query: types.CallbackQuery, state: FSMContext):
method_name = callback_query.data.split("|")[1] method_name = callback_query.data.split("|")[1]
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
@@ -125,32 +151,32 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
await state.update_data(heleket_method=method_name) await state.update_data(heleket_method=method_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2): for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS): if i + 1 < len(PAYMENT_OPTIONS):
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"heleket_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
) )
) )
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}")) builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"heleket_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_heleket")) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_heleket_crypto"))
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=method["desc"], text=method["desc"],
@@ -165,10 +191,10 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext):
method_name = callback_query.data.split("|")[1] method_name = callback_query.data.split("|")[1]
await state.update_data(heleket_method=method_name) await state.update_data(heleket_method=method_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_heleket_{method_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_heleket_{method_name}"))
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=ENTER_SUM, text=ENTER_SUM,
@@ -183,7 +209,7 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
data = await state.get_data() data = await state.get_data()
method_name = data.get("heleket_method") method_name = data.get("heleket_method")
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=message, target_message=message,
@@ -192,12 +218,12 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
force_text=True, force_text=True,
) )
return return
try: try:
amount = int(message.text.strip()) amount = int(message.text.strip())
if amount <= 0: if amount <= 0:
raise ValueError raise ValueError
if amount < 10: if amount < 10:
await edit_or_send_message( await edit_or_send_message(
target_message=message, target_message=message,
text="Минимальная сумма для оплаты криптовалютой — 10 рублей.", text="Минимальная сумма для оплаты криптовалютой — 10 рублей.",
@@ -213,24 +239,77 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
force_text=True, force_text=True,
) )
return return
await state.update_data(amount=amount) await state.update_data(amount=amount)
payment_url = await generate_heleket_payment_link(amount, message.chat.id, method) payment_url = await generate_heleket_payment_link(amount, message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup( confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")], [InlineKeyboardButton(text=BACK, callback_data="balance")],
] ]
) )
await edit_or_send_message( await edit_or_send_message(
target_message=message, target_message=message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount), text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard, reply_markup=confirm_keyboard,
force_text=True, force_text=True,
) )
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
async def process_fast_flow_heleket(
callback_query: types.CallbackQuery,
state: FSMContext,
session: AsyncSession,
amount: int,
method_name: str = "crypto",
):
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
if not method:
await edit_or_send_message(
target_message=callback_query.message,
text="Ошибка: выбранный способ оплаты недоступен.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
if amount <= 0:
await edit_or_send_message(
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
if amount < 10:
await edit_or_send_message(
target_message=callback_query.message,
text="Минимальная сумма для оплаты криптовалютой — 10 рублей.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
await state.update_data(heleket_method=method_name, amount=amount)
payment_url = await generate_heleket_payment_link(amount, callback_query.message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")],
]
)
await edit_or_send_message(
target_message=callback_query.message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard,
force_text=True,
)
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation) await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
@@ -239,9 +318,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
parts = callback_query.data.split("|") parts = callback_query.data.split("|")
method_name = parts[1] method_name = parts[1]
amount_str = parts[2] amount_str = parts[2]
method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in HELEKET_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
@@ -250,7 +329,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
try: try:
amount = int(amount_str) amount = int(amount_str)
if amount <= 0: if amount <= 0:
@@ -263,68 +342,84 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
await state.update_data(amount=amount) await state.update_data(amount=amount)
payment_url = await generate_heleket_payment_link(amount, callback_query.message.chat.id, method) payment_url = await generate_heleket_payment_link(amount, callback_query.message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup( confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")], [InlineKeyboardButton(text=BACK, callback_data="balance")],
] ]
) )
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=HELEKET_PAYMENT_MESSAGE.format(amount=amount), text=HELEKET_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard, reply_markup=confirm_keyboard,
force_text=True, force_text=True,
) )
await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation) await state.set_state(ReplenishBalanceHeleket.waiting_for_payment_confirmation)
async def generate_heleket_payment_link(amount: int, tg_id: int, method: dict) -> str: async def generate_heleket_payment_link(amount: int, tg_id: int, method: dict) -> str:
""" """
Создание платежа в Heleket и получение ссылки на оплату Создание платежа в Heleket и получение ссылки на оплату.
amount сумма в RUB, method['currency'] валюта провайдера (обычно USD).
""" """
url = "https://api.heleket.com/v1/payment" url = "https://api.heleket.com/v1/payment"
unique_order_id = f"{int(time.time())}_{tg_id}" unique_order_id = f"{int(time.time())}_{tg_id}"
usd_amount = round(amount / HELEKET_CURRENCY_RATE, 2)
data = {
"amount": str(usd_amount),
"currency": method["currency"],
"order_id": unique_order_id,
"url_success": HELEKET_SUCCESS_URL,
"url_return": HELEKET_RETURN_URL,
"url_callback": HELEKET_CALLBACK_URL,
"additional_data": f"tg_id:{tg_id},rub_amount:{amount}",
}
if method.get("to_currency"):
data["to_currency"] = method["to_currency"]
json_data = json.dumps(data, separators=(',', ':'))
base64_data = base64.b64encode(json_data.encode('utf-8')).decode('utf-8')
sign_string = base64_data + HELEKET_API_KEY
signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
headers = {
"merchant": HELEKET_MERCHANT_ID,
"sign": signature,
"Content-Type": "application/json",
}
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
pay_cur = str(method["currency"]).upper()
if pay_cur == "RUB":
payment_amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
else:
rate = await get_rub_rate(pay_cur, session=session)
payment_amount = (Decimal(str(amount)) * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
async with async_session_maker() as dbs:
await add_payment(
session=dbs,
tg_id=tg_id,
amount=float(amount),
payment_system="HELEKET",
status="pending",
currency="RUB",
payment_id=unique_order_id,
)
data = {
"amount": str(payment_amount),
"currency": method["currency"],
"order_id": unique_order_id,
"url_success": HELEKET_SUCCESS_URL,
"url_return": HELEKET_RETURN_URL,
"url_callback": HELEKET_CALLBACK_URL,
"additional_data": f"tg_id:{tg_id},rub_amount:{amount}",
}
if method.get("to_currency"):
data["to_currency"] = method["to_currency"]
json_data = json.dumps(data, separators=(",", ":"))
base64_data = base64.b64encode(json_data.encode("utf-8")).decode("utf-8")
sign_string = base64_data + HELEKET_API_KEY
signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest()
headers = {
"merchant": HELEKET_MERCHANT_ID,
"sign": signature,
"Content-Type": "application/json",
}
async with session.post(url, headers=headers, data=json_data, timeout=60) as resp: async with session.post(url, headers=headers, data=json_data, timeout=60) as resp:
if resp.status == 200: if resp.status == 200:
try: try:
resp_json = await resp.json() resp_json = await resp.json()
if resp_json.get("state") == 0: if resp_json.get("state") == 0:
payment_url = resp_json.get("result", {}).get("url") payment_url = resp_json.get("result", {}).get("url")
if payment_url: if payment_url:
logger.info(f"Heleket payment URL created for user {tg_id}") logger.info(f"Heleket payment URL created for user {tg_id}")
@@ -350,4 +445,4 @@ async def generate_heleket_payment_link(amount: int, tg_id: int, method: dict) -
return "https://heleket.com/" return "https://heleket.com/"
except Exception as e: except Exception as e:
logger.error(f"Error creating Heleket payment: {e}") logger.error(f"Error creating Heleket payment: {e}")
return "https://heleket.com/" return "https://heleket.com/"
@@ -1,7 +1,8 @@
import aiohttp
import hashlib import hashlib
import hmac import hmac
import time import time
import aiohttp
from aiogram import F, Router, types from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
@@ -10,14 +11,23 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import ( from config import (
KASSAI_ENABLE, KASSAI_API_KEY, KASSAI_SECRET_KEY, KASSAI_DOMAIN, KASSAI_SHOP_ID, KASSAI_API_KEY,
KASSAI_IP, KASSAI_SUCCESS_URL, KASSAI_FAILURE_URL KASSAI_DOMAIN,
KASSAI_FAILURE_URL,
KASSAI_IP,
KASSAI_SECRET_KEY,
KASSAI_SHOP_ID,
KASSAI_SUCCESS_URL,
PROVIDERS_ENABLED,
) )
from handlers.payments.providers import get_providers
from handlers.buttons import BACK, PAY_2, KASSAI_CARDS, KASSAI_SBP from handlers.buttons import BACK, KASSAI_CARDS, KASSAI_SBP, PAY_2
from handlers.texts import ( from handlers.texts import (
KASSAI_CARDS_DESCRIPTION, KASSAI_SBP_DESCRIPTION, ENTER_SUM,
KASSAI_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS KASSAI_CARDS_DESCRIPTION,
KASSAI_PAYMENT_MESSAGE,
KASSAI_SBP_DESCRIPTION,
PAYMENT_OPTIONS,
) )
from handlers.utils import edit_or_send_message from handlers.utils import edit_or_send_message
from logger import logger from logger import logger
@@ -32,20 +42,35 @@ class ReplenishBalanceKassaiState(StatesGroup):
entering_custom_amount = State() entering_custom_amount = State()
PROVIDERS = get_providers(PROVIDERS_ENABLED)
KASSAI_PAYMENT_METHODS = [ KASSAI_PAYMENT_METHODS = [
{"enable": KASSAI_ENABLE, "method": 36, "name": "cards", "button": KASSAI_CARDS, "desc": KASSAI_CARDS_DESCRIPTION}, {
{"enable": KASSAI_ENABLE, "method": 44, "name": "sbp", "button": KASSAI_SBP, "desc": KASSAI_SBP_DESCRIPTION}, "enable": bool(PROVIDERS.get("KASSAI_CARDS", {}).get("enabled")),
"method": 36,
"name": "cards",
"button": KASSAI_CARDS,
"desc": KASSAI_CARDS_DESCRIPTION,
},
{
"enable": bool(PROVIDERS.get("KASSAI_SBP", {}).get("enabled")),
"method": 44,
"name": "sbp",
"button": KASSAI_SBP,
"desc": KASSAI_SBP_DESCRIPTION,
},
] ]
@router.callback_query(F.data == "pay_kassai") @router.callback_query(F.data == "pay_kassai")
async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None): async def process_callback_pay_kassai(
callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, method_name: str = None
):
try: try:
tg_id = callback_query.message.chat.id tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated KassaAI payment.") logger.info(f"User {tg_id} initiated KassaAI payment.")
await state.clear() await state.clear()
if method_name: if method_name:
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None) method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name and m["enable"]), None)
if not method: if not method:
@@ -54,45 +79,42 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state
except Exception: except Exception:
pass pass
await callback_query.message.answer( await callback_query.message.answer(
target_message=callback_query.message,
text="Ошибка: выбранный способ оплаты недоступен.", text="Ошибка: выбранный способ оплаты недоступен.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]), reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
) )
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2): for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS): if i + 1 < len(PAYMENT_OPTIONS):
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
) )
) )
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}")) builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
try: try:
await callback_query.message.delete() await callback_query.message.delete()
except Exception: except Exception:
pass pass
new_msg = await callback_query.message.answer( new_msg = await callback_query.message.answer(
target_message=callback_query.message,
text=method["desc"], text=method["desc"],
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
) )
await state.update_data( await state.update_data(
kassai_method=method_name, kassai_method=method_name,
message_id=new_msg.message_id, message_id=new_msg.message_id,
@@ -100,25 +122,24 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state
) )
await state.set_state(ReplenishBalanceKassaiState.choosing_amount) await state.set_state(ReplenishBalanceKassaiState.choosing_amount)
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for method in KASSAI_PAYMENT_METHODS: for method in KASSAI_PAYMENT_METHODS:
if method["enable"]: if method["enable"]:
builder.row(InlineKeyboardButton(text=method["button"], callback_data=f'kassai_method|{method["name"]}')) builder.row(InlineKeyboardButton(text=method["button"], callback_data=f"kassai_method|{method['name']}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
try: try:
await callback_query.message.delete() await callback_query.message.delete()
except Exception: except Exception:
pass pass
new_msg = await callback_query.message.answer( new_msg = await callback_query.message.answer(
target_message=callback_query.message,
text="Выберите способ оплаты через KassaAI:", text="Выберите способ оплаты через KassaAI:",
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
) )
await state.update_data(message_id=new_msg.message_id, chat_id=new_msg.chat.id) await state.update_data(message_id=new_msg.message_id, chat_id=new_msg.chat.id)
await state.set_state(ReplenishBalanceKassaiState.choosing_method) await state.set_state(ReplenishBalanceKassaiState.choosing_method)
except Exception as e: except Exception as e:
logger.error(f"Error in process_callback_pay_kassai for user {callback_query.message.chat.id}: {e}") logger.error(f"Error in process_callback_pay_kassai for user {callback_query.message.chat.id}: {e}")
await callback_query.answer("Произошла ошибка при инициализации платежа. Попробуйте позже.", show_alert=True) await callback_query.answer("Произошла ошибка при инициализации платежа. Попробуйте позже.", show_alert=True)
@@ -128,7 +149,7 @@ async def process_callback_pay_kassai(callback_query: types.CallbackQuery, state
async def process_method_selection(callback_query: types.CallbackQuery, state: FSMContext): async def process_method_selection(callback_query: types.CallbackQuery, state: FSMContext):
method_name = callback_query.data.split("|")[1] method_name = callback_query.data.split("|")[1]
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
@@ -137,32 +158,32 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
await state.update_data(kassai_method=method_name) await state.update_data(kassai_method=method_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2): for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS): if i + 1 < len(PAYMENT_OPTIONS):
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i + 1]['callback_data'].split('|')[1]}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]["callback_data"].split("|")[1]}', callback_data=f"kassai_amount|{method_name}|{PAYMENT_OPTIONS[i]['callback_data'].split('|')[1]}",
) )
) )
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}")) builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"kassai_custom_amount|{method_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_kassai")) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay_kassai"))
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=method["desc"], text=method["desc"],
@@ -177,10 +198,10 @@ async def process_method_selection(callback_query: types.CallbackQuery, state: F
async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext):
method_name = callback_query.data.split("|")[1] method_name = callback_query.data.split("|")[1]
await state.update_data(kassai_method=method_name) await state.update_data(kassai_method=method_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_kassai_{method_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_kassai_{method_name}"))
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=ENTER_SUM, text=ENTER_SUM,
@@ -195,7 +216,7 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
data = await state.get_data() data = await state.get_data()
method_name = data.get("kassai_method") method_name = data.get("kassai_method")
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=message, target_message=message,
@@ -204,7 +225,7 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
force_text=True, force_text=True,
) )
return return
try: try:
amount = int(message.text.strip()) amount = int(message.text.strip())
if amount <= 0: if amount <= 0:
@@ -225,24 +246,24 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
force_text=True, force_text=True,
) )
return return
await state.update_data(amount=amount) await state.update_data(amount=amount)
payment_url = await generate_kassai_payment_link(amount, message.chat.id, method) payment_url = await generate_kassai_payment_link(amount, message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup( confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")], [InlineKeyboardButton(text=BACK, callback_data="balance")],
] ]
) )
payment_message = await edit_or_send_message( await edit_or_send_message(
target_message=message, target_message=message,
text=KASSAI_PAYMENT_MESSAGE.format(amount=amount), text=KASSAI_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard, reply_markup=confirm_keyboard,
force_text=True, force_text=True,
) )
await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation) await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation)
@@ -251,9 +272,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
parts = callback_query.data.split("|") parts = callback_query.data.split("|")
method_name = parts[1] method_name = parts[1]
amount_str = parts[2] amount_str = parts[2]
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None) method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
if not method or not method["enable"]: if not method or not method["enable"]:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
@@ -262,7 +283,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
try: try:
amount = int(amount_str) amount = int(amount_str)
if amount <= 0: if amount <= 0:
@@ -275,24 +296,24 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
await state.update_data(amount=amount) await state.update_data(amount=amount)
payment_url = await generate_kassai_payment_link(amount, callback_query.message.chat.id, method) payment_url = await generate_kassai_payment_link(amount, callback_query.message.chat.id, method)
confirm_keyboard = InlineKeyboardMarkup( confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")], [InlineKeyboardButton(text=BACK, callback_data="balance")],
] ]
) )
payment_message = await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=KASSAI_PAYMENT_MESSAGE.format(amount=amount), text=KASSAI_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard, reply_markup=confirm_keyboard,
force_text=True, force_text=True,
) )
await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation) await state.set_state(ReplenishBalanceKassaiState.waiting_for_payment_confirmation)
@@ -303,14 +324,12 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
nonce = int(time.time()) nonce = int(time.time())
unique_payment_id = f"{nonce}_{tg_id}" unique_payment_id = f"{nonce}_{tg_id}"
url = "https://api.fk.life/v1/orders/create" url = "https://api.fk.life/v1/orders/create"
headers = { headers = {"Content-Type": "application/json"}
"Content-Type": "application/json",
}
client_email = f"{tg_id}@{KASSAI_DOMAIN}" client_email = f"{tg_id}@{KASSAI_DOMAIN}"
client_ip = KASSAI_IP client_ip = KASSAI_IP
data_for_signature = { data_for_signature = {
"shopId": KASSAI_SHOP_ID, "shopId": KASSAI_SHOP_ID,
"nonce": nonce, "nonce": nonce,
@@ -321,22 +340,14 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
"currency": "RUB", "currency": "RUB",
"success_url": KASSAI_SUCCESS_URL, "success_url": KASSAI_SUCCESS_URL,
"failure_url": KASSAI_FAILURE_URL, "failure_url": KASSAI_FAILURE_URL,
"paymentId": unique_payment_id "paymentId": unique_payment_id,
} }
sorted_keys = sorted(data_for_signature.keys()) sign_string = "|".join(str(data_for_signature[k]) for k in sorted(data_for_signature.keys()))
values = [str(data_for_signature[key]) for key in sorted_keys] signature = hmac.new(KASSAI_API_KEY.encode("utf-8"), sign_string.encode("utf-8"), hashlib.sha256).hexdigest()
sign_string = "|".join(values)
data = {**data_for_signature, "signature": signature}
signature = hmac.new(
KASSAI_API_KEY.encode('utf-8'),
sign_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
data = data_for_signature.copy()
data["signature"] = signature
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data, timeout=60) as resp: async with session.post(url, headers=headers, json=data, timeout=60) as resp:
@@ -348,12 +359,10 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
if payment_url: if payment_url:
logger.info(f"KassaAI payment URL created for user {tg_id}") logger.info(f"KassaAI payment URL created for user {tg_id}")
return payment_url return payment_url
else: logger.error(f"KassaAI: No location in response: {resp_json}")
logger.error(f"KassaAI: No location in response: {resp_json}")
return "https://fk.life/"
else:
logger.error(f"KassaAI: Unsuccessful response: {resp_json}")
return "https://fk.life/" return "https://fk.life/"
logger.error(f"KassaAI: Unsuccessful response: {resp_json}")
return "https://fk.life/"
except Exception as e: except Exception as e:
logger.error(f"KassaAI: Error parsing JSON response: {e}") logger.error(f"KassaAI: Error parsing JSON response: {e}")
text = await resp.text() text = await resp.text()
@@ -374,26 +383,17 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
def verify_kassai_signature(data: dict, signature: str) -> bool: def verify_kassai_signature(data: dict, signature: str) -> bool:
""" """
Проверка подписи вебхука KassaAI согласно документации FreeKassa Проверка подписи вебхука FreeKassa (используемой KassaAI):
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
""" """
try: try:
sign_string = ( sign_string = f"{KASSAI_SHOP_ID}:{data.get('AMOUNT', '')}:{KASSAI_SECRET_KEY}:{data.get('MERCHANT_ORDER_ID', '')}"
f"{KASSAI_SHOP_ID}:" expected_signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest()
f"{data.get('AMOUNT', '')}:"
f"{KASSAI_SECRET_KEY}:"
f"{data.get('MERCHANT_ORDER_ID', '')}"
)
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
result = signature.upper() == expected_signature.upper() result = signature.upper() == expected_signature.upper()
if not result: if not result:
logger.error(f"KassaAI signature mismatch. Expected: {expected_signature}, Got: {signature}") logger.error(f"KassaAI signature mismatch. Expected: {expected_signature}, Got: {signature}")
logger.error(f"Sign string: {sign_string}") logger.error(f"Sign string: {sign_string}")
return result return result
except Exception as e: except Exception as e:
logger.error(f"Ошибка проверки подписи KassaAI: {e}") logger.error(f"Ошибка проверки подписи KassaAI: {e}")
return False return False
+96
View File
@@ -0,0 +1,96 @@
from collections.abc import Iterable
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RENEWAL_PRICES
from handlers.buttons import BACK, CUSTOM_AMOUNT
from handlers.payments.currency_rates import format_for_user
async def payment_options_for_user(
db_session,
tg_id: int,
language_code: str | None,
*,
force_currency: str | None = None,
) -> list[dict]:
items = []
for price_rub in RENEWAL_PRICES.values():
txt = await format_for_user(
db_session,
tg_id,
price_rub,
language_code,
force_currency=force_currency,
)
items.append({"text": txt, "callback_data": f"amount|{int(price_rub)}"})
return items
def payment_options(currency: str = "RUB") -> list[dict]:
return [{"text": f"{price} {currency}", "callback_data": f"amount|{price}"} for price in RENEWAL_PRICES.values()]
def build_amounts_keyboard(
*,
prefix: str,
pattern: str,
back_cb: str = "balance",
custom_cb: str | tuple[str, str] | None = None,
per_row: int = 2,
opts: Iterable[dict] | None = None,
) -> InlineKeyboardMarkup:
items = list(opts) if opts is not None else payment_options()
b = InlineKeyboardBuilder()
row = []
for i, item in enumerate(items, 1):
row.append(
InlineKeyboardButton(
text=item["text"],
callback_data=pattern.format(prefix=prefix, price=item["callback_data"].split("|", 1)[-1]),
)
)
if i % per_row == 0:
b.row(*row)
row = []
if row:
b.row(*row)
if custom_cb:
cb = custom_cb[1] if isinstance(custom_cb, tuple) else custom_cb
b.row(InlineKeyboardButton(text=CUSTOM_AMOUNT, callback_data=cb))
b.row(InlineKeyboardButton(text=BACK, callback_data=back_cb))
return b.as_markup()
def parse_amount_from_callback(data: str, *, prefixes: list[str]) -> int | None:
for p in prefixes:
if data.startswith(f"{p}_amount|"):
try:
return int(data.split("|", 1)[1])
except Exception:
return None
if data.startswith(f"{p}|amount|"):
try:
return int(data.rsplit("|", 1)[-1])
except Exception:
return None
if data.startswith(f"{p}|"):
try:
return int(data.split("|", 1)[1])
except Exception:
return None
return None
def pay_keyboard(url: str, *, pay_text: str, back_cb: str) -> InlineKeyboardMarkup:
b = InlineKeyboardBuilder()
b.row(InlineKeyboardButton(text=pay_text, url=url))
b.row(InlineKeyboardButton(text=BACK, callback_data=back_cb))
return b.as_markup()
def back_keyboard(back_cb: str) -> InlineKeyboardMarkup:
b = InlineKeyboardBuilder()
b.row(InlineKeyboardButton(text=BACK, callback_data=back_cb))
return b.as_markup()
+202
View File
@@ -0,0 +1,202 @@
import os
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import MULTICURRENCY_ENABLE, DONATIONS_ENABLE, PROVIDERS_ENABLED
from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG
from database import get_last_payments
from database.models import User
from handlers import buttons as btn
from handlers.payments.heleket.heleket import process_callback_pay_heleket
from handlers.payments.kassai.kassai import process_callback_pay_kassai
from handlers.payments.stars.handlers import process_callback_pay_stars
from handlers.payments.tribute.handlers import process_callback_pay_tribute
from handlers.payments.wata.wata import process_callback_pay_wata
from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks
from handlers.payments.currency_rates import format_for_user
from handlers.payments.currency_flow import build_currency_choice_kb
from handlers.payments.providers import get_providers_with_hooks
from ..utils import edit_or_send_message
router = Router()
@router.callback_query(F.data == "pay")
async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
payment_handlers = []
for key, cfg in PROVIDERS.items():
if not cfg.get("enabled"):
continue
val = cfg.get("value")
if not val:
continue
fn = globals().get(f"process_callback_{val}")
if callable(fn):
payment_handlers.append(fn)
module_buttons = await run_hooks("pay_menu_buttons", chat_id=callback_query.from_user.id, admin=False, session=session)
has_extra_menu_items = bool(module_buttons) or bool(DONATIONS_ENABLE) or PROVIDERS.get("TRIBUTE", {}).get("enabled")
if MULTICURRENCY_ENABLE:
kb = build_currency_choice_kb(show_stars=bool(PROVIDERS.get("STARS", {}).get("enabled")), prefix="pay_currency")
await edit_or_send_message(
target_message=callback_query.message,
text=FAST_PAY_CHOOSE_CURRENCY,
reply_markup=kb.as_markup()
)
return
if len(payment_handlers) == 1 and not has_extra_menu_items:
return await payment_handlers[0](callback_query, state, session)
builder = InlineKeyboardBuilder()
for key, cfg in PROVIDERS.items():
if not cfg.get("enabled"):
continue
text = getattr(btn, key, key)
builder.row(InlineKeyboardButton(text=text, callback_data=cfg["value"]))
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder = insert_hook_buttons(builder, module_buttons)
builder.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
await edit_or_send_message(target_message=callback_query.message, text=PAYMENT_METHODS_MSG, reply_markup=builder.as_markup())
async def _build_pay_menu_for_currency(currency: str) -> InlineKeyboardBuilder:
PROVIDERS = await get_providers_with_hooks(PROVIDERS_ENABLED)
b = InlineKeyboardBuilder()
for key, cfg in PROVIDERS.items():
if not cfg.get("enabled"):
continue
if cfg.get("currency") != currency:
continue
text = getattr(btn, key, key)
b.row(InlineKeyboardButton(text=text, callback_data=cfg["value"]))
return b
@router.callback_query(F.data.startswith("pay_currency|"))
async def handle_pay_currency(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
currency = callback_query.data.split("|")[1]
if currency == "STARS":
return await process_callback_pay_stars(callback_query, state, session)
base_builder = await _build_pay_menu_for_currency(currency)
module_buttons = await run_hooks("pay_menu_buttons", chat_id=callback_query.from_user.id, admin=False, session=session)
builder = insert_hook_buttons(base_builder, module_buttons)
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text=btn.BACK, callback_data="back_to_currency"))
builder.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
await edit_or_send_message(target_message=callback_query.message, text=PAYMENT_METHODS_MSG, reply_markup=builder.as_markup())
@router.callback_query(F.data == "balance")
async def balance_handler(callback_query: CallbackQuery, session: AsyncSession):
stmt = select(User.balance).where(User.tg_id == callback_query.from_user.id)
result = await session.execute(stmt)
balance_rub = result.scalar_one_or_none() or 0.0
language_code = getattr(callback_query.from_user, "language_code", None)
balance_text = await format_for_user(session, callback_query.from_user.id, balance_rub, language_code)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=btn.PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=btn.BALANCE_HISTORY, callback_data="balance_history"))
builder.row(InlineKeyboardButton(text=btn.COUPON, callback_data="activate_coupon"))
builder.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
text = BALANCE_MANAGEMENT_TEXT.format(balance=balance_text)
image_path = os.path.join("img", "pay.jpg")
await edit_or_send_message(target_message=callback_query.message, text=text, reply_markup=builder.as_markup(), media_path=image_path, disable_web_page_preview=False)
@router.callback_query(F.data == "balance_history")
async def balance_history_handler(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=btn.PAYMENT, callback_data="pay"))
builder.row(InlineKeyboardButton(text=btn.MAIN_MENU, callback_data="profile"))
records = await get_last_payments(session, callback_query.from_user.id, statuses=["success"])
if records:
language_code = getattr(callback_query.from_user, "language_code", None)
history_text = "<b>💳 История операций:</b>\n\n<blockquote>"
for record in records:
amount_rub = record["amount"] or 0
formatted_amount = await format_for_user(session, callback_query.from_user.id, amount_rub, language_code)
payment_system = record["payment_system"]
status = record["status"]
date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S")
history_text += f"Сумма: {formatted_amount}\nОплата: {payment_system}\nСтатус: {status}\nДата: {date}\n\n"
history_text += "</blockquote>"
else:
history_text = "❌ У вас пока нет операций с балансом."
await edit_or_send_message(target_message=callback_query.message, text=history_text, reply_markup=builder.as_markup(), media_path=None, disable_web_page_preview=False)
@router.callback_query(F.data == "back_to_currency")
async def back_to_currency(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
kb = build_currency_choice_kb(show_stars=True, prefix="pay_currency")
kb.row(InlineKeyboardButton(text=btn.BACK, callback_data="back_to_pay"))
await edit_or_send_message(
target_message=callback_query.message,
text=FAST_PAY_CHOOSE_CURRENCY,
reply_markup=kb.as_markup()
)
@router.callback_query(F.data == "pay_wata_ru")
async def handle_pay_wata_ru(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="ru")
@router.callback_query(F.data == "pay_wata_sbp")
async def handle_pay_wata_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="sbp")
@router.callback_query(F.data == "pay_wata_int")
async def handle_pay_wata_int(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_wata(callback_query, state, session, cassa_name="int")
@router.callback_query(F.data == "pay_kassai_cards")
async def handle_pay_kassai_cards(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_kassai(callback_query, state, session, method_name="cards")
@router.callback_query(F.data == "pay_kassai_sbp")
async def handle_pay_kassai_sbp(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_kassai(callback_query, state, session, method_name="sbp")
@router.callback_query(F.data == "pay_heleket_crypto")
async def handle_pay_heleket_crypto(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_heleket(callback_query, state, session, method_name="crypto")
@router.callback_query(F.data == "pay_tribute")
async def handle_pay_tribute(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
await process_callback_pay_tribute(callback_query, state, session)
+93
View File
@@ -0,0 +1,93 @@
from typing import Dict
from hooks.hooks import run_hooks
PROVIDERS_BASE: Dict[str, dict] = {
"YOOKASSA": {
"currency": "RUB",
"value": "pay_yookassa",
"fast": "process_custom_amount_input",
},
"YOOMONEY": {
"currency": "RUB",
"value": "pay_yoomoney",
"fast": "process_custom_amount_input_yoomoney",
},
"ROBOKASSA": {
"currency": "RUB",
"value": "pay_robokassa",
"fast": "handle_custom_amount_input",
},
"KASSAI_CARDS": {
"currency": "RUB",
"value": "pay_kassai_cards",
"fast": None,
},
"KASSAI_SBP": {
"currency": "RUB",
"value": "pay_kassai_sbp",
"fast": None,
},
"WATA_RU": {
"currency": "RUB",
"value": "pay_wata_ru",
"fast": None,
},
"WATA_SBP": {
"currency": "RUB",
"value": "pay_wata_sbp",
"fast": None,
},
"TRIBUTE": {
"currency": "RUB",
"value": "pay_tribute",
"fast": None,
},
"HELEKET": {
"currency": "USD",
"value": "pay_heleket_crypto",
"fast": None,
},
"CRYPTOBOT": {
"currency": "USD",
"value": "pay_cryptobot",
"fast": "process_custom_amount_input",
},
"FREEKASSA": {
"currency": "USD",
"value": "pay_freekassa",
"fast": None,
},
"WATA_INT": {
"currency": "USD",
"value": "pay_wata_int",
"fast": None,
},
"STARS": {
"currency": "STARS",
"value": "pay_stars",
"fast": "process_custom_amount_input_stars",
},
}
def get_providers(flags: Dict[str, bool]) -> Dict[str, dict]:
out: Dict[str, dict] = {}
for k, base in PROVIDERS_BASE.items():
cfg = dict(base)
cfg["enabled"] = bool(flags.get(k))
out[k] = cfg
return out
async def get_providers_with_hooks(flags: Dict[str, bool]) -> Dict[str, dict]:
out = get_providers(flags)
results = await run_hooks("providers_config", providers=out, flags=flags)
for r in results:
if not isinstance(r, dict):
continue
for name, patch in r.items():
if patch is None:
out.pop(name, None)
elif isinstance(patch, dict):
base = dict(out.get(name, {}))
base.update(patch)
out[name] = base
return out
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+162
View File
@@ -0,0 +1,162 @@
from typing import Any
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from sqlalchemy.ext.asyncio import AsyncSession
from database import add_user, check_user_exists, get_key_count, get_temporary_data
from handlers.buttons import CUSTOM_AMOUNT, PAY_2, MAIN_MENU
from handlers.payments.keyboards import (
back_keyboard,
build_amounts_keyboard,
parse_amount_from_callback,
pay_keyboard as build_pay_keyboard,
payment_options_for_user,
)
from handlers.texts import DEFAULT_PAYMENT_MESSAGE, ENTER_SUM
from handlers.payments.currency_rates import format_for_user
from handlers.utils import edit_or_send_message
from logger import logger
from .service import create_and_store_robokassa_payment
router = Router()
class ReplenishBalanceState(StatesGroup):
choosing_amount_robokassa = State()
waiting_for_payment_confirmation_robokassa = State()
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
tg_id = callback_query.message.chat.id
b = await get_key_count(session, tg_id)
if b == 0 and not await check_user_exists(session, tg_id):
u = callback_query.from_user
await add_user(
tg_id=u.id,
username=u.username,
first_name=u.first_name,
last_name=u.last_name,
language_code=u.language_code,
is_bot=u.is_bot,
session=session,
)
logger.info(f"[DB] Новый пользователь {tg_id} создан через Robokassa.")
language_code = getattr(callback_query.from_user, "language_code", None)
opts = await payment_options_for_user(session, tg_id, language_code, force_currency="RUB")
markup = build_amounts_keyboard(
prefix="robokassa",
pattern="{prefix}_amount|{price}",
back_cb="balance",
custom_cb=(CUSTOM_AMOUNT, "enter_custom_amount_robokassa"),
per_row=2,
opts=opts,
)
await callback_query.message.delete()
m = await callback_query.message.answer(text="Выберите сумму пополнения:", reply_markup=markup)
await state.update_data(message_id=m.message_id, chat_id=m.chat.id)
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
@router.callback_query(F.data.startswith("robokassa_"))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession):
amount = parse_amount_from_callback(callback_query.data, prefixes=["robokassa"])
if not amount or amount <= 0:
await edit_or_send_message(
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
force_text=True,
)
return
tg_id = callback_query.message.chat.id
url, _ = await create_and_store_robokassa_payment(session, tg_id, amount, "Пополнение баланса", inv_id=0)
kb = build_pay_keyboard(
url,
pay_text=PAY_2,
back_cb="pay_robokassa",
)
language_code = getattr(callback_query.from_user, "language_code", None)
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
await edit_or_send_message(
target_message=callback_query.message,
text=DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text),
reply_markup=kb,
force_text=True,
)
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
b = back_keyboard("pay_robokassa")
await edit_or_send_message(target_message=callback_query.message, text=ENTER_SUM, reply_markup=b, force_text=True)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(
event: types.Message | types.CallbackQuery,
session: AsyncSession,
pay_button_text: str = PAY_2,
main_menu_text: str = MAIN_MENU,
):
if isinstance(event, types.CallbackQuery):
message = event.message
from_user = event.from_user
tg_id = from_user.id
temp_data = await get_temporary_data(session, tg_id)
if not temp_data or temp_data["state"] not in ["waiting_for_payment", "waiting_for_renewal_payment", "waiting_for_gift_payment"]:
await edit_or_send_message(target_message=message, text="❌ Не удалось получить данные для оплаты.")
return
amount = int(temp_data["data"].get("required_amount", 0))
if amount <= 0:
await edit_or_send_message(target_message=message, text="❌ Не удалось определить сумму оплаты.")
return
else:
message = event
from_user = message.from_user
tg_id = from_user.id
text = message.text
if not text or not text.isdigit():
await message.answer("Введите корректную сумму числом.")
return
amount = int(text)
if amount <= 0:
await message.answer("Сумма должна быть больше нуля.")
return
try:
url, _ = await create_and_store_robokassa_payment(session, tg_id, amount, "Пополнение баланса", inv_id=0)
markup = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=pay_button_text, url=url)],
[InlineKeyboardButton(text=main_menu_text, callback_data="profile")],
]
)
language_code = getattr(from_user, "language_code", None)
amount_text = await format_for_user(session, tg_id, float(amount), language_code, force_currency="RUB")
text_out = DEFAULT_PAYMENT_MESSAGE.format(amount=amount_text)
await edit_or_send_message(target_message=message, text=text_out, reply_markup=markup)
except Exception as e:
from logger import logger as _lg
_lg.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
await edit_or_send_message(
target_message=message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[]),
)
+87
View File
@@ -0,0 +1,87 @@
import hashlib
import json
import uuid
from decimal import ROUND_DOWN, Decimal
from urllib.parse import quote_plus, urlencode
from sqlalchemy.ext.asyncio import AsyncSession
from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from database import add_payment
def _build_receipt(amount: float, sno: str = "usn_income") -> dict:
return {
"items": [
{
"name": "Пополнение баланса",
"quantity": 1,
"sum": float(amount),
"payment_method": "full_payment",
"payment_object": "payment",
"tax": "none",
}
],
"sno": sno,
}
def _format_amount(amount: float | int) -> str:
s = str(Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_DOWN))
return s.rstrip("0").rstrip(".") if "." in s else s
def generate_payment_link(amount: int | float, inv_id: int, description: str, tg_id: int) -> tuple[str, str]:
out_sum = _format_amount(amount)
receipt_json = json.dumps(_build_receipt(amount), ensure_ascii=False, separators=(",", ":"))
receipt_enc = quote_plus(receipt_json, safe="")
pid = str(uuid.uuid4())
shp = {"Shp_id": str(tg_id), "Shp_pid": pid}
base = f"{ROBOKASSA_LOGIN}:{out_sum}:{inv_id}:{receipt_enc}:{ROBOKASSA_PASSWORD1}"
for k in sorted(shp.keys(), key=str.lower):
base += f":{k}={shp[k]}"
signature = hashlib.md5(base.encode("utf-8")).hexdigest().upper()
query = {
"MrchLogin": ROBOKASSA_LOGIN,
"OutSum": out_sum,
"InvId": inv_id,
"Description": description,
"Receipt": receipt_enc,
"SignatureValue": signature,
**shp,
}
if ROBOKASSA_TEST_MODE:
query["IsTest"] = 1
return "https://auth.robokassa.ru/Merchant/Index.aspx?" + urlencode(query), pid
async def create_and_store_robokassa_payment(
session: AsyncSession, tg_id: int, amount: int | float, description: str, inv_id: int = 0
) -> tuple[str, str]:
url, pid = generate_payment_link(amount, inv_id, description, tg_id)
await add_payment(
session=session,
tg_id=tg_id,
amount=float(amount),
payment_system="robokassa",
status="pending",
currency="RUB",
payment_id=pid,
metadata=None,
)
return url, pid
def check_payment_signature(params) -> bool:
out_sum = params.get("OutSum") or params.get("out_summ") or params.get("outsumm")
inv_id = params.get("InvId") or params.get("inv_id") or params.get("invid")
received_sig = (params.get("SignatureValue") or params.get("signaturevalue") or "").upper()
if not out_sum or not inv_id or not received_sig:
return False
shp_items = [(k, params[k]) for k in params.keys() if k.lower().startswith("shp_")]
shp_items.sort(key=lambda kv: kv[0].lower())
shp_suffix = "".join(f":{k}={v}" for k, v in shp_items)
base = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}{shp_suffix}"
expected_sig = hashlib.md5(base.encode("utf-8")).hexdigest().upper()
return received_sig == expected_sig
+53
View File
@@ -0,0 +1,53 @@
from aiohttp import web
from database import add_payment, async_session_maker, get_payment_by_payment_id, update_balance, update_payment_status
from handlers.payments.utils import send_payment_success_notification
from logger import logger
from .service import check_payment_signature
async def robokassa_webhook(request: web.Request):
try:
params = await request.post()
if not check_payment_signature(params):
return web.Response(status=400)
amount_raw = params.get("OutSum")
inv_id = params.get("InvId")
shp_id = params.get("Shp_id") or params.get("shp_id") or params.get("id")
shp_pid = params.get("Shp_pid") or params.get("shp_pid") or params.get("pid")
if not amount_raw or not inv_id or not shp_id or not shp_pid:
return web.Response(status=400)
tg_id = int(shp_id)
amount = float(amount_raw)
async with async_session_maker() as session:
payment = await get_payment_by_payment_id(session, shp_pid)
if payment:
if payment.get("status") == "success":
return web.Response(text=f"OK{inv_id}")
ok = await update_payment_status(session=session, internal_id=int(payment["id"]), new_status="success")
if not ok:
return web.Response(status=500)
else:
await add_payment(
session=session,
tg_id=tg_id,
amount=amount,
payment_system="ROBOKASSA",
status="success",
currency="RUB",
payment_id=shp_pid,
metadata=None,
)
await update_balance(session, tg_id, amount)
await send_payment_success_notification(tg_id, amount, session)
return web.Response(text=f"OK{inv_id}")
except Exception as e:
logger.error(f"Error processing ROBOKASSA webhook: {e}")
return web.Response(status=500)
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
View File
@@ -1,4 +1,5 @@
import aiohttp import aiohttp
from aiogram import F, Router, types from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
@@ -7,17 +8,23 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from config import ( from config import (
WATA_RU_ENABLE, WATA_RU_TOKEN,
WATA_SBP_ENABLE, WATA_SBP_TOKEN,
WATA_INT_ENABLE, WATA_INT_TOKEN,
REDIRECT_LINK,
FAIL_REDIRECT_LINK, FAIL_REDIRECT_LINK,
REDIRECT_LINK,
WATA_INT_TOKEN,
WATA_RU_TOKEN,
WATA_SBP_TOKEN,
PROVIDERS_ENABLED,
) )
from handlers.payments.providers import get_providers
from handlers.buttons import BACK, PAY_2, WATA_RU, WATA_SBP, WATA_INT from handlers.buttons import BACK, PAY_2, WATA_INT, WATA_RU, WATA_SBP
from handlers.texts import ( from handlers.texts import (
WATA_RU_DESCRIPTION, WATA_SBP_DESCRIPTION, WATA_INT_DESCRIPTION, ENTER_SUM,
WATA_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS, WATA_PAYMENT_TITLE PAYMENT_OPTIONS,
WATA_INT_DESCRIPTION,
WATA_PAYMENT_MESSAGE,
WATA_PAYMENT_TITLE,
WATA_RU_DESCRIPTION,
WATA_SBP_DESCRIPTION,
) )
from handlers.utils import edit_or_send_message from handlers.utils import edit_or_send_message
from logger import logger from logger import logger
@@ -30,20 +37,42 @@ class ReplenishBalanceWataState(StatesGroup):
choosing_cassa = State() choosing_cassa = State()
choosing_amount = State() choosing_amount = State()
waiting_for_payment_confirmation = State() waiting_for_payment_confirmation = State()
entering_custom_amount = State() entering_custom_amount = State()
PROVIDERS = get_providers(PROVIDERS_ENABLED)
WATA_CASSA_CONFIG = [ WATA_CASSA_CONFIG = [
{"enable": WATA_RU_ENABLE, "token": WATA_RU_TOKEN, "name": "ru", "button": WATA_RU, "desc": WATA_RU_DESCRIPTION}, {
{"enable": WATA_SBP_ENABLE, "token": WATA_SBP_TOKEN, "name": "sbp", "button": WATA_SBP, "desc": WATA_SBP_DESCRIPTION}, "enable": bool(PROVIDERS.get("WATA_RU", {}).get("enabled")),
{"enable": WATA_INT_ENABLE, "token": WATA_INT_TOKEN, "name": "int", "button": WATA_INT, "desc": WATA_INT_DESCRIPTION}, "token": WATA_RU_TOKEN,
"name": "ru",
"button": WATA_RU,
"desc": WATA_RU_DESCRIPTION,
},
{
"enable": bool(PROVIDERS.get("WATA_SBP", {}).get("enabled")),
"token": WATA_SBP_TOKEN,
"name": "sbp",
"button": WATA_SBP,
"desc": WATA_SBP_DESCRIPTION,
},
{
"enable": bool(PROVIDERS.get("WATA_INT", {}).get("enabled")),
"token": WATA_INT_TOKEN,
"name": "int",
"button": WATA_INT,
"desc": WATA_INT_DESCRIPTION,
},
] ]
@router.callback_query(F.data == "pay_wata") @router.callback_query(F.data == "pay_wata")
async def process_callback_pay_wata(callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, cassa_name: str = None): async def process_callback_pay_wata(
callback_query: types.CallbackQuery, state: FSMContext, session: AsyncSession, cassa_name: str = None
):
tg_id = callback_query.message.chat.id tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} initiated WATA payment.") logger.info(f"User {tg_id} initiated WATA payment.")
if cassa_name: if cassa_name:
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name and c["enable"]), None) cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name and c["enable"]), None)
if not cassa: if not cassa:
@@ -54,29 +83,36 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state:
force_text=True, force_text=True,
) )
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2): for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS): if i + 1 < len(PAYMENT_OPTIONS):
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]['callback_data']}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]['callback_data']}",
) )
) )
builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"wata_custom_amount|{cassa_name}")) builder.row(InlineKeyboardButton(text="Ввести сумму", callback_data=f"wata_custom_amount|{cassa_name}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="pay")) builder.row(InlineKeyboardButton(text=BACK, callback_data="pay"))
await callback_query.message.delete()
try:
await callback_query.message.delete()
except Exception:
pass
new_message = await callback_query.message.answer( new_message = await callback_query.message.answer(
text=cassa["desc"], text=cassa["desc"],
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
@@ -84,12 +120,18 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state:
await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id, wata_cassa=cassa_name) await state.update_data(message_id=new_message.message_id, chat_id=new_message.chat.id, wata_cassa=cassa_name)
await state.set_state(ReplenishBalanceWataState.choosing_amount) await state.set_state(ReplenishBalanceWataState.choosing_amount)
return return
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for cassa in WATA_CASSA_CONFIG: for cassa in WATA_CASSA_CONFIG:
if cassa["enable"]: if cassa["enable"]:
builder.row(InlineKeyboardButton(text=cassa["button"], callback_data=f'wata_cassa|{cassa["name"]}')) builder.row(InlineKeyboardButton(text=cassa["button"], callback_data=f"wata_cassa|{cassa['name']}"))
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
await callback_query.message.delete()
try:
await callback_query.message.delete()
except Exception:
pass
new_message = await callback_query.message.answer( new_message = await callback_query.message.answer(
text="Выберите способ оплаты через WATA:", text="Выберите способ оплаты через WATA:",
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
@@ -101,8 +143,9 @@ async def process_callback_pay_wata(callback_query: types.CallbackQuery, state:
@router.callback_query(F.data.startswith("wata_cassa|")) @router.callback_query(F.data.startswith("wata_cassa|"))
async def process_cassa_selection(callback_query: types.CallbackQuery, state: FSMContext): async def process_cassa_selection(callback_query: types.CallbackQuery, state: FSMContext):
cassa_name = callback_query.data.split("|")[1] cassa_name = callback_query.data.split("|")[1]
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name), None) cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name and c["enable"]), None)
if not cassa or not cassa["enable"]:
if not cassa:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text="Ошибка: выбранная касса недоступна.", text="Ошибка: выбранная касса недоступна.",
@@ -110,29 +153,36 @@ async def process_cassa_selection(callback_query: types.CallbackQuery, state: FS
force_text=True, force_text=True,
) )
return return
await state.update_data(wata_cassa=cassa_name) await state.update_data(wata_cassa=cassa_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2): for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS): if i + 1 < len(PAYMENT_OPTIONS):
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]['callback_data']}",
), ),
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"], text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i + 1]['callback_data']}",
), ),
) )
else: else:
builder.row( builder.row(
InlineKeyboardButton( InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"], text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]["callback_data"]}', callback_data=f"wata_amount|{cassa_name}|{PAYMENT_OPTIONS[i]['callback_data']}",
) )
) )
builder.row(InlineKeyboardButton(text=BACK, callback_data="balance")) builder.row(InlineKeyboardButton(text=BACK, callback_data="balance"))
await callback_query.message.delete()
try:
await callback_query.message.delete()
except Exception:
pass
new_message = await callback_query.message.answer( new_message = await callback_query.message.answer(
text=cassa["desc"], text=cassa["desc"],
reply_markup=builder.as_markup(), reply_markup=builder.as_markup(),
@@ -161,8 +211,10 @@ async def handle_custom_amount_text_input(message: types.Message, state: FSMCont
async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext): async def process_custom_amount_button(callback_query: types.CallbackQuery, state: FSMContext):
cassa_name = callback_query.data.split("|")[1] cassa_name = callback_query.data.split("|")[1]
await state.update_data(wata_cassa=cassa_name) await state.update_data(wata_cassa=cassa_name)
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_wata_{cassa_name}")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"pay_wata_{cassa_name}"))
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=ENTER_SUM, text=ENTER_SUM,
@@ -231,8 +283,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
parts = callback_query.data.split("|") parts = callback_query.data.split("|")
cassa_name = parts[1] cassa_name = parts[1]
amount_str = parts[-1] amount_str = parts[-1]
cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name), None)
if not cassa or not cassa["enable"]: cassa = next((c for c in WATA_CASSA_CONFIG if c["name"] == cassa_name and c["enable"]), None)
if not cassa:
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text="Ошибка: выбранная касса недоступна.", text="Ошибка: выбранная касса недоступна.",
@@ -240,6 +293,7 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
try: try:
amount = int(amount_str) amount = int(amount_str)
if amount <= 0: if amount <= 0:
@@ -252,20 +306,24 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
force_text=True, force_text=True,
) )
return return
await state.update_data(amount=amount) await state.update_data(amount=amount)
payment_url = await generate_wata_payment_link(amount, callback_query.message.chat.id, cassa) payment_url = await generate_wata_payment_link(amount, callback_query.message.chat.id, cassa)
confirm_keyboard = InlineKeyboardMarkup( confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[InlineKeyboardButton(text=PAY_2, url=payment_url)], [InlineKeyboardButton(text=PAY_2, url=payment_url)],
[InlineKeyboardButton(text=BACK, callback_data="balance")], [InlineKeyboardButton(text=BACK, callback_data="balance")],
] ]
) )
await edit_or_send_message( await edit_or_send_message(
target_message=callback_query.message, target_message=callback_query.message,
text=WATA_PAYMENT_MESSAGE.format(amount=amount), text=WATA_PAYMENT_MESSAGE.format(amount=amount),
reply_markup=confirm_keyboard, reply_markup=confirm_keyboard,
force_text=True, force_text=True,
) )
await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation) await state.set_state(ReplenishBalanceWataState.waiting_for_payment_confirmation)
@@ -287,51 +345,49 @@ async def generate_wata_payment_link(amount, tg_id, cassa):
if cassa["name"] == "int": if cassa["name"] == "int":
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from datetime import datetime from datetime import datetime
async def get_usd_rate(): async def get_usd_rate():
today = datetime.now().strftime("%d/%m/%Y") today = datetime.now().strftime("%d/%m/%Y")
url = f"http://www.cbr.ru/scripts/XML_daily.asp?date_req={today}" url = f"http://www.cbr.ru/scripts/XML_daily.asp?date_req={today}"
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=15) as resp: async with session.get(url, timeout=15) as resp:
if resp.status == 200: if resp.status == 200:
xml_content = await resp.text() xml_content = await resp.text()
root = ET.fromstring(xml_content) root = ET.fromstring(xml_content)
for valute in root.findall('Valute'): for valute in root.findall("Valute"):
char_code = valute.find('CharCode') char_code = valute.find("CharCode")
if char_code is not None and char_code.text == 'USD': if char_code is not None and char_code.text == "USD":
value_elem = valute.find('Value') value_elem = valute.find("Value")
if value_elem is not None: if value_elem is not None:
usd_rub_rate = float(value_elem.text.replace(",", "."))
usd_rub_rate = float(value_elem.text.replace(',', '.'))
rub_usd_rate = 1 / usd_rub_rate rub_usd_rate = 1 / usd_rub_rate
logger.info(f"Successfully got USD rate from CBR: 1 USD = {usd_rub_rate} RUB, 1 RUB = {rub_usd_rate} USD") logger.info(
f"CBR USD rate: 1 USD = {usd_rub_rate} RUB, 1 RUB = {rub_usd_rate} USD"
)
return rub_usd_rate return rub_usd_rate
logger.warning("USD rate not found in CBR response") logger.warning("USD rate not found in CBR response")
except Exception as e: except Exception as e:
logger.error(f"Failed to get USD rate from CBR: {e}") logger.error(f"Failed to get USD rate from CBR: {e}")
fallback_rate = 0.0105 fallback_rate = 0.0105
logger.warning(f"Using fallback USD rate: {fallback_rate}") logger.warning(f"Using fallback USD rate: {fallback_rate}")
return fallback_rate return fallback_rate
try: try:
usd_rate = await get_usd_rate() usd_rate = await get_usd_rate()
rub_per_usd = 1 / usd_rate rub_per_usd = 1 / usd_rate
rub_per_usd_plus_5 = rub_per_usd + 5 rub_per_usd_plus_5 = rub_per_usd + 5
new_usd_rate = 1 / rub_per_usd_plus_5 new_usd_rate = 1 / rub_per_usd_plus_5
amount_usd = round(float(amount) * new_usd_rate, 2) amount_usd = round(float(amount) * new_usd_rate, 2)
data["amount"] = amount_usd data["amount"] = amount_usd
data["currency"] = "USD" data["currency"] = "USD"
except Exception as e: except Exception as e:
logger.error(f"Failed to convert RUB to USD: {e}") logger.error(f"Failed to convert RUB to USD: {e}")
fallback_usd_rate = 0.0105 fallback_usd_rate = 0.0105
rub_per_usd = 1 / fallback_usd_rate rub_per_usd = 1 / fallback_usd_rate
rub_per_usd_plus_5 = rub_per_usd + 5 rub_per_usd_plus_5 = rub_per_usd + 5
@@ -339,6 +395,7 @@ async def generate_wata_payment_link(amount, tg_id, cassa):
amount_usd = round(float(amount) * new_usd_rate, 2) amount_usd = round(float(amount) * new_usd_rate, 2)
data["amount"] = amount_usd data["amount"] = amount_usd
data["currency"] = "USD" data["currency"] = "USD"
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data, timeout=60) as resp: async with session.post(url, headers=headers, json=data, timeout=60) as resp:
if resp.status == 200: if resp.status == 200:
@@ -348,17 +405,17 @@ async def generate_wata_payment_link(amount, tg_id, cassa):
text = await resp.text() text = await resp.text()
logger.error(f"Ошибка при разборе JSON ответа WATA: статус={resp.status}, ответ={text}") logger.error(f"Ошибка при разборе JSON ответа WATA: статус={resp.status}, ответ={text}")
return "https://wata.pro/" return "https://wata.pro/"
if "url" in resp_json: if "url" in resp_json:
return resp_json["url"] return resp_json["url"]
else:
logger.error(f"Ответ WATA без url: {resp_json}")
return "https://wata.pro/"
else:
try: logger.error(f"Ответ WATA без url: {resp_json}")
error_json = await resp.json()
logger.error(f"Ошибка WATA API: статус={resp.status}, ответ={error_json}")
except Exception:
text = await resp.text()
logger.error(f"Ошибка WATA API: статус={resp.status}, не-JSON ответ: {text}")
return "https://wata.pro/" return "https://wata.pro/"
try:
error_json = await resp.json()
logger.error(f"Ошибка WATA API: статус={resp.status}, ответ={error_json}")
except Exception:
text = await resp.text()
logger.error(f"Ошибка WATA API: статус={resp.status}, не-JSON ответ: {text}")
return "https://wata.pro/"
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+3
View File
@@ -0,0 +1,3 @@
__all__ = ("router",)
from .handlers import router
+29 -9
View File
@@ -1,3 +1,4 @@
import asyncio
import os import os
from aiogram import F, Router from aiogram import F, Router
@@ -23,10 +24,11 @@ from handlers.buttons import (
GIFTS, GIFTS,
INSTRUCTIONS, INSTRUCTIONS,
INVITE, INVITE,
MY_SUB,
MY_SUBS, MY_SUBS,
RENEW_KEY,
TRIAL_SUB, TRIAL_SUB,
) )
from handlers.payments.currency_rates import format_for_user
from handlers.texts import ADD_SUBSCRIPTION_HINT from handlers.texts import ADD_SUBSCRIPTION_HINT
from hooks.hook_buttons import insert_hook_buttons from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks from hooks.hooks import run_hooks
@@ -59,22 +61,40 @@ async def process_callback_view_profile(
chat_id = chat.id chat_id = chat.id
username = get_username(user or chat) username = get_username(user or chat)
key_count = await get_key_count(session, chat_id) key_count, balance_rub, trial_status = await asyncio.gather(
balance = await get_balance(session, chat_id) or 0 get_key_count(session, chat_id),
trial_status = await get_trial(session, chat_id) get_balance(session, chat_id),
get_trial(session, chat_id),
)
balance_rub = balance_rub or 0
profile_message = profile_message_send(username, chat_id, int(balance), key_count) fmt_task = asyncio.create_task(format_for_user(session, chat_id, balance_rub, getattr(user, "language_code", None)))
profile_menu_task = asyncio.create_task(run_hooks("profile_menu", chat_id=chat_id, admin=admin, session=session))
profile_text_task = asyncio.create_task(
run_hooks(
"profile_text",
username=username,
chat_id=chat_id,
balance=int(balance_rub),
key_count=key_count,
session=session,
)
)
balance_text = await fmt_task
profile_message = profile_message_send(username, chat_id, balance_text, key_count)
profile_message += ADD_SUBSCRIPTION_HINT if key_count == 0 else f"\n<blockquote><i>{NEWS_MESSAGE}</i></blockquote>" profile_message += ADD_SUBSCRIPTION_HINT if key_count == 0 else f"\n<blockquote><i>{NEWS_MESSAGE}</i></blockquote>"
text_hooks = await run_hooks("profile_text", username=username, chat_id=chat_id, balance=int(balance), key_count=key_count, session=session) text_hooks = await profile_text_task
if text_hooks: if text_hooks:
profile_message = text_hooks[0] profile_message = text_hooks[0]
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
if key_count > 0: if key_count > 0:
builder.row(InlineKeyboardButton(text=RENEW_KEY, callback_data="renew_menu")) subs_label = MY_SUB if key_count == 1 else MY_SUBS
builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys")) builder.row(InlineKeyboardButton(text=subs_label, callback_data="view_keys"))
elif trial_status == 0 and not TRIAL_TIME_DISABLE: elif trial_status == 0 and not TRIAL_TIME_DISABLE:
builder.row(InlineKeyboardButton(text=TRIAL_SUB, callback_data="create_key")) builder.row(InlineKeyboardButton(text=TRIAL_SUB, callback_data="create_key"))
else: else:
@@ -91,7 +111,7 @@ async def process_callback_view_profile(
if extra_buttons: if extra_buttons:
builder.row(*extra_buttons) builder.row(*extra_buttons)
module_buttons = await run_hooks("profile_menu", chat_id=chat_id, admin=admin, session=session) module_buttons = await profile_menu_task
builder = insert_hook_buttons(builder, module_buttons) builder = insert_hook_buttons(builder, module_buttons)
if INSTRUCTIONS_BUTTON: if INSTRUCTIONS_BUTTON:
+9 -3
View File
@@ -31,6 +31,7 @@ from database import (
from database.models import Referral from database.models import Referral
from database.tariffs import get_tariffs from database.tariffs import get_tariffs
from handlers.buttons import BACK, INVITE, MAIN_MENU, QR, TOP_FIVE from handlers.buttons import BACK, INVITE, MAIN_MENU, QR, TOP_FIVE
from handlers.payments.currency_rates import format_for_user
from handlers.texts import ( from handlers.texts import (
INVITE_MESSAGE_TEMPLATE, INVITE_MESSAGE_TEMPLATE,
INVITE_TEXT_NON_INLINE, INVITE_TEXT_NON_INLINE,
@@ -54,9 +55,11 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery, ses
if isinstance(callback_query_or_message, CallbackQuery): if isinstance(callback_query_or_message, CallbackQuery):
chat_id = callback_query_or_message.message.chat.id chat_id = callback_query_or_message.message.chat.id
target_message = callback_query_or_message.message target_message = callback_query_or_message.message
language_code = callback_query_or_message.from_user.language_code
else: else:
chat_id = callback_query_or_message.chat.id chat_id = callback_query_or_message.chat.id
target_message = callback_query_or_message target_message = callback_query_or_message
language_code = callback_query_or_message.from_user.language_code
referral_link = get_referral_link(chat_id) referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(session, chat_id) referral_stats = await get_referral_stats(session, chat_id)
@@ -66,7 +69,8 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery, ses
if isinstance(value, float): if isinstance(value, float):
bonuses_lines.append(f"{level} уровень: 🌟 {int(value * 100)}% бонуса") bonuses_lines.append(f"{level} уровень: 🌟 {int(value * 100)}% бонуса")
else: else:
bonuses_lines.append(f"{level} уровень: 💸 {int(value)}₽ бонуса") value_txt = await format_for_user(session, chat_id, value, language_code)
bonuses_lines.append(f"{level} уровень: 💸 {value_txt} бонуса")
bonuses_block = "\n".join(bonuses_lines) bonuses_block = "\n".join(bonuses_lines)
details_lines = [] details_lines = []
@@ -75,16 +79,18 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery, ses
if isinstance(bonus_value, float): if isinstance(bonus_value, float):
bonus_str = f"{int(bonus_value * 100)}%" bonus_str = f"{int(bonus_value * 100)}%"
else: else:
bonus_str = f"{int(bonus_value)}" bonus_str = await format_for_user(session, chat_id, bonus_value, language_code)
details_lines.append(f"🔹 Уровень {level}: {stats['total']} - {bonus_str}") details_lines.append(f"🔹 Уровень {level}: {stats['total']} - {bonus_str}")
details_block = "\n".join(details_lines) details_block = "\n".join(details_lines)
total_bonus_txt = await format_for_user(session, chat_id, referral_stats["total_referral_bonus"], language_code)
invite_message = INVITE_MESSAGE_TEMPLATE.format( invite_message = INVITE_MESSAGE_TEMPLATE.format(
referral_link=referral_link, referral_link=referral_link,
bonuses_block=bonuses_block, bonuses_block=bonuses_block,
total_referrals=referral_stats["total_referrals"], total_referrals=referral_stats["total_referrals"],
details_block=details_block, details_block=details_block,
total_referral_bonus=referral_stats["total_referral_bonus"], total_referral_bonus=total_bonus_txt,
) )
image_path = os.path.join("img", "pic_invite.jpg") image_path = os.path.join("img", "pic_invite.jpg")
+17 -18
View File
@@ -1,3 +1,4 @@
import asyncio
import os import os
from typing import Any from typing import Any
@@ -43,7 +44,6 @@ from handlers.captcha import generate_captcha
from handlers.coupons import activate_coupon from handlers.coupons import activate_coupon
from handlers.payments.gift import handle_gift_link from handlers.payments.gift import handle_gift_link
from handlers.profile import process_callback_view_profile from handlers.profile import process_callback_view_profile
from hooks.hook_buttons import insert_hook_buttons
from handlers.texts import ( from handlers.texts import (
NOT_SUBSCRIBED_YET_MSG, NOT_SUBSCRIBED_YET_MSG,
SUBSCRIPTION_CHECK_ERROR_MSG, SUBSCRIPTION_CHECK_ERROR_MSG,
@@ -52,6 +52,7 @@ from handlers.texts import (
WELCOME_TEXT, WELCOME_TEXT,
get_about_vpn, get_about_vpn,
) )
from hooks.hook_buttons import insert_hook_buttons
from hooks.hooks import run_hooks from hooks.hooks import run_hooks
from logger import logger from logger import logger
@@ -115,6 +116,8 @@ async def process_start_logic(
if text.startswith("/start "): if text.startswith("/start "):
text = text.split(maxsplit=1)[1] text = text.split(maxsplit=1)[1]
await state.update_data(original_text=text, user_data=user_data)
gift_detected = False gift_detected = False
for part in text.split("-"): for part in text.split("-"):
await run_hooks("start_link", message=message, state=state, session=session, user_data=user_data, part=part) await run_hooks("start_link", message=message, state=state, session=session, user_data=user_data, part=part)
@@ -138,16 +141,18 @@ async def process_start_logic(
if not await check_user_exists(session, user_data["tg_id"]): if not await check_user_exists(session, user_data["tg_id"]):
await add_user(session=session, **user_data) await add_user(session=session, **user_data)
trial_status = await get_trial(session, user_data["tg_id"]) trial_status, key_count = await asyncio.gather(
key_count = await get_key_count(session, user_data["tg_id"]) get_trial(session, user_data["tg_id"]),
get_key_count(session, user_data["tg_id"]),
)
if SHOW_START_MENU_ONCE: if SHOW_START_MENU_ONCE:
if key_count > 0 or trial_status != 0: if key_count > 0 or trial_status != 0:
await process_callback_view_profile(message, state, admin, session) await process_callback_view_profile(message, state, admin, session)
else: else:
await show_start_menu(message, admin, session) await show_start_menu(message, admin, session, trial_status=trial_status)
else: else:
await show_start_menu(message, admin, session) await show_start_menu(message, admin, session, trial_status=trial_status)
async def handle_coupon_link(part, message, state, session, admin, user_data): async def handle_coupon_link(part, message, state, session, admin, user_data):
@@ -173,12 +178,8 @@ async def handle_gift(part, message, state, session, user_data):
processing_gifts.add(gift_id) processing_gifts.add(gift_id)
try: try:
gift_results = await run_hooks("gift_activation", gift_results = await run_hooks(
gift_id=gift_id, "gift_activation", gift_id=gift_id, message=message, state=state, session=session, user_data=user_data
message=message,
state=state,
session=session,
user_data=user_data
) )
if gift_results and "SUCCESS" in gift_results: if gift_results and "SUCCESS" in gift_results:
@@ -232,11 +233,13 @@ async def handle_utm_link(utm_code: str, message: Message, state: FSMContext, se
await add_user(session=session, source_code=utm_code, **user_data) await add_user(session=session, source_code=utm_code, **user_data)
async def show_start_menu(message: Message, admin: bool, session: AsyncSession): async def show_start_menu(message: Message, admin: bool, session: AsyncSession, trial_status: int | None = None):
image_path = os.path.join("img", "pic.jpg") image_path = os.path.join("img", "pic.jpg")
kb = InlineKeyboardBuilder() kb = InlineKeyboardBuilder()
trial_status = await get_trial(session, message.chat.id) if session else None if trial_status is None:
trial_status = await get_trial(session, message.chat.id) if session else None
show_trial = trial_status == 0 and not TRIAL_TIME_DISABLE show_trial = trial_status == 0 and not TRIAL_TIME_DISABLE
show_profile = not SHOW_START_MENU_ONCE or trial_status != 0 or TRIAL_TIME_DISABLE show_profile = not SHOW_START_MENU_ONCE or trial_status != 0 or TRIAL_TIME_DISABLE
@@ -292,9 +295,5 @@ async def handle_about_vpn(callback: CallbackQuery, session: AsyncSession):
text = text_hooks[0] text = text_hooks[0]
await edit_or_send_message( await edit_or_send_message(
callback.message, callback.message, text, reply_markup=kb.as_markup(), media_path=os.path.join("img", "pic.jpg"), force_text=False
text,
reply_markup=kb.as_markup(),
media_path=os.path.join("img", "pic.jpg"),
force_text=False
) )
+3 -3
View File
@@ -309,15 +309,15 @@ def format_discount_time_left(last_time: datetime, discount_hours: int) -> str:
expires_at = last_time + timedelta(hours=discount_hours) expires_at = last_time + timedelta(hours=discount_hours)
current_time = datetime.utcnow() current_time = datetime.utcnow()
time_left = expires_at - current_time time_left = expires_at - current_time
if time_left.total_seconds() <= 0: if time_left.total_seconds() <= 0:
return "⏳ Время истекло" return "⏳ Время истекло"
total_seconds = int(time_left.total_seconds()) total_seconds = int(time_left.total_seconds())
days = total_seconds // 86400 days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600 hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60 minutes = (total_seconds % 3600) // 60
if days > 0: if days > 0:
return format_days(days) return format_days(days)
elif hours > 0: elif hours > 0:
+2 -2
View File
@@ -19,7 +19,7 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline
buttons = buttons or [] buttons = buttons or []
flat_buttons = [] flat_buttons = []
for item in buttons: for item in buttons:
if isinstance(item, (list, tuple)): if isinstance(item, list | tuple):
flat_buttons.extend(item) flat_buttons.extend(item)
else: else:
flat_buttons.append(item) flat_buttons.append(item)
@@ -48,7 +48,7 @@ def insert_hook_buttons(builder: InlineKeyboardBuilder, buttons: list) -> Inline
for module in insert_operations: for module in insert_operations:
insert_at = module["insert_at"] insert_at = module["insert_at"]
button = module["button"] button = module["button"]
if 0 <= insert_at <= len(new_rows): if 0 <= insert_at <= len(new_rows):
new_rows.insert(insert_at, [button]) new_rows.insert(insert_at, [button])
else: else:
+4
View File
@@ -54,3 +54,7 @@ logger.add(
) )
logger = logger logger = logger
logging.getLogger("apscheduler").setLevel(logging.WARNING)
logging.getLogger("apscheduler.executors.default").setLevel(logging.WARNING)
logging.getLogger("apscheduler.scheduler").setLevel(logging.WARNING)
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,13 +8,13 @@ from middlewares.ban_checker import BanCheckerMiddleware
from middlewares.subscription import SubscriptionMiddleware from middlewares.subscription import SubscriptionMiddleware
from .admin import AdminMiddleware from .admin import AdminMiddleware
from .answer import CallbackAnswerMiddleware
from .direct_start_blocker import DirectStartBlockerMiddleware from .direct_start_blocker import DirectStartBlockerMiddleware
from .loggings import LoggingMiddleware from .loggings import LoggingMiddleware
from .maintenance import MaintenanceModeMiddleware from .maintenance import MaintenanceModeMiddleware
from .session import SessionMiddleware from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware from .throttling import ThrottlingMiddleware
from .user import UserMiddleware from .user import UserMiddleware
from .answer import CallbackAnswerMiddleware
def register_middleware( def register_middleware(
+70 -46
View File
@@ -14,6 +14,8 @@ from logger import logger
TZ = timezone("Europe/Moscow") TZ = timezone("Europe/Moscow")
_BAN_CACHE_TTL = 30
_ban_cache: dict[int, tuple[float, dict | None]] = {}
class BanCheckerMiddleware(BaseMiddleware): class BanCheckerMiddleware(BaseMiddleware):
@@ -43,53 +45,75 @@ class BanCheckerMiddleware(BaseMiddleware):
if tg_id is None: if tg_id is None:
return await handler(event, data) return await handler(event, data)
async with self.session_factory() as session: now_ts = datetime.utcnow().timestamp()
logger.debug(f"[BanChecker] Проверка блокировки для пользователя {tg_id}") cached = _ban_cache.get(tg_id)
result = await session.execute( if cached and cached[0] > now_ts:
select(ManualBan).where( ban_info = cached[1]
ManualBan.tg_id == tg_id, else:
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()), session: AsyncSession | None = (
) data.get("session") if isinstance(data.get("session"), AsyncSession) else None
) )
ban = result.scalar_one_or_none() created_here = False
if session is None:
if ban: session = self.session_factory()
reason = ban.reason or "не указана" created_here = True
until = ban.until try:
q = (
if reason == "shadow": select(ManualBan.reason, ManualBan.until)
logger.info(f"[BanChecker] Теневой бан: пользователь {tg_id} — действия игнорируются.") .where(
return ManualBan.tg_id == tg_id,
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.utcnow()),
logger.warning(f"[BanChecker] Пользователь {tg_id} заблокирован (до: {until}, причина: {reason})")
if until:
until_local = until.astimezone(TZ).strftime("%Y-%m-%d %H:%M")
text_html = (
f"🚫 Вы заблокированы до <b>{until_local}</b> по МСК.\n"
f"📄 Причина: <i>{reason}</i>\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
text_plain = (
f"🚫 Вы заблокированы до {until_local} по МСК.\n"
f"📄 Причина: {reason}\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
) )
.limit(1)
)
res = await session.execute(q)
row = res.first()
if row:
reason, until = row
ban_info = {"reason": reason or "не указана", "until": until}
else: else:
text_html = ( ban_info = None
f"🚫 Вы заблокированы <b>навсегда</b>.\n" finally:
f"📄 Причина: <i>{reason}</i>\n\n" if created_here:
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}" await session.close()
) _ban_cache[tg_id] = (now_ts + _BAN_CACHE_TTL, ban_info)
text_plain = (
f"🚫 Вы заблокированы навсегда.\n"
f"📄 Причина: {reason}\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
if isinstance(obj, Message): if not ban_info:
await obj.answer(text_html, parse_mode="HTML") return await handler(event, data)
elif isinstance(obj, CallbackQuery):
await obj.answer(text_plain, show_alert=True) reason = ban_info["reason"]
return until = ban_info["until"]
return await handler(event, data)
if reason == "shadow":
logger.info(f"[BanChecker] Теневой бан: пользователь {tg_id} — действия игнорируются.")
return
if until:
until_local = until.astimezone(TZ).strftime("%Y-%m-%d %H:%M")
text_html = (
f"🚫 Вы заблокированы до <b>{until_local}</b> по МСК.\n"
f"📄 Причина: <i>{reason}</i>\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
text_plain = (
f"🚫 Вы заблокированы до {until_local} по МСК.\n"
f"📄 Причина: {reason}\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
else:
text_html = (
f"🚫 Вы заблокированы <b>навсегда</b>.\n"
f"📄 Причина: <i>{reason}</i>\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
text_plain = (
f"🚫 Вы заблокированы навсегда.\n"
f"📄 Причина: {reason}\n\n"
f"Если вы считаете, что это ошибка, обратитесь в поддержку: {SUPPORT_CHAT_URL}"
)
if isinstance(obj, Message):
await obj.answer(text_html, parse_mode="HTML")
elif isinstance(obj, CallbackQuery):
await obj.answer(text_plain, show_alert=True)
return
+118 -62
View File
@@ -1,10 +1,12 @@
import time
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import datetime
from typing import Any from typing import Any
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from aiogram.types import Message, Update from aiogram.types import Message, Update
from sqlalchemy import select from sqlalchemy import select
from datetime import datetime
from config import DISABLE_DIRECT_START from config import DISABLE_DIRECT_START
from database import async_session_maker, check_user_exists from database import async_session_maker, check_user_exists
@@ -12,6 +14,14 @@ from database.models import Coupon, Gift, TrackingSource, User
from logger import logger from logger import logger
_TTL = 20
_cache_user_exists: dict[int, tuple[float, bool]] = {}
_cache_coupon: dict[str, tuple[float, bool]] = {}
_cache_gift: dict[str, tuple[float, bool]] = {}
_cache_ref: dict[int, tuple[float, bool]] = {}
_cache_utm: dict[str, tuple[float, bool]] = {}
class DirectStartBlockerMiddleware(BaseMiddleware): class DirectStartBlockerMiddleware(BaseMiddleware):
def __init__(self) -> None: def __init__(self) -> None:
self.allowed_prefixes = ("gift_", "referral_", "coupons_", "utm", "partner_") self.allowed_prefixes = ("gift_", "referral_", "coupons_", "utm", "partner_")
@@ -22,8 +32,6 @@ class DirectStartBlockerMiddleware(BaseMiddleware):
event: Update, event: Update,
data: dict[str, Any], data: dict[str, Any],
) -> Any: ) -> Any:
logger.debug("[DirectStartBlocker] Middleware triggered")
if not DISABLE_DIRECT_START: if not DISABLE_DIRECT_START:
return await handler(event, data) return await handler(event, data)
@@ -31,72 +39,120 @@ class DirectStartBlockerMiddleware(BaseMiddleware):
if not message or not message.text: if not message or not message.text:
return await handler(event, data) return await handler(event, data)
fsm = data.get("state")
if fsm:
current_state = await fsm.get_state()
if current_state:
return await handler(event, data)
tg_id = message.from_user.id tg_id = message.from_user.id
text = message.text.strip() text = message.text.strip()
now = time.time()
async with async_session_maker() as session: async def user_exists_cached() -> bool:
user_exists = await check_user_exists(session, tg_id) cached = _cache_user_exists.get(tg_id)
if cached and cached[0] > now:
return cached[1]
async with async_session_maker() as session:
exists = await check_user_exists(session, tg_id)
_cache_user_exists[tg_id] = (now + _TTL, exists)
return exists
if user_exists: if not text.startswith("/"):
logger.debug(f"[DirectStartBlocker] Пользователь {tg_id} уже есть в базе — пропущен") return await handler(event, data)
parts = text.split(maxsplit=1)
if parts[0] != "/start":
if await user_exists_cached():
return await handler(event, data) return await handler(event, data)
parts = text.split(maxsplit=1)
if parts[0] == "/start":
if len(parts) == 1:
logger.info(f"[DirectStartBlocker] Прямой старт запрещён для нового пользователя {tg_id}")
return
start_param = parts[1].strip()
if not start_param or not start_param.startswith(self.allowed_prefixes):
logger.info(f"[DirectStartBlocker] Отклонена неизвестная ссылка от {tg_id}: {start_param!r}")
return
if start_param.startswith("coupons_"):
code = start_param.removeprefix("coupons_")
result = await session.execute(select(Coupon).where(Coupon.code == code))
if not result.scalar_one_or_none():
logger.info(f"[DirectStartBlocker] Купон не найден: {code!r}")
return
elif start_param.startswith("gift_"):
gift_id = start_param.removeprefix("gift_")
result = await session.execute(
select(Gift).where(
Gift.gift_id == gift_id,
Gift.is_used.is_(False),
(Gift.expiry_time.is_(None)) | (Gift.expiry_time > datetime.utcnow()),
)
)
if not result.scalar_one_or_none():
logger.info(f"[DirectStartBlocker] Подарок неактивен или не найден: {gift_id!r}")
return
elif start_param.startswith("referral_"):
try:
ref_id = int(start_param.removeprefix("referral_"))
result = await session.execute(select(User).where(User.tg_id == ref_id))
if not result.scalar_one_or_none():
logger.info(f"[DirectStartBlocker] Реферал не найден: {ref_id!r}")
return
except ValueError:
logger.info(f"[DirectStartBlocker] Неверный формат referral-ссылки: {start_param!r}")
return
elif start_param.startswith("utm"):
utm_code = start_param
result = await session.execute(select(TrackingSource).where(TrackingSource.code == utm_code))
if not result.scalar_one_or_none():
logger.info(f"[DirectStartBlocker] UTM не найден: {utm_code!r}")
return
logger.info(f"[DirectStartBlocker] Разрешённая и валидная ссылка от {tg_id}: {start_param!r}")
return await handler(event, data)
if text.startswith("/"):
logger.info( logger.info(
f"[DirectStartBlocker] Команда '{text}' отклонена для незарегистрированного пользователя {tg_id}" f"[DirectStartBlocker] Команда '{text}' отклонена для незарегистрированного пользователя {tg_id}"
) )
return return
return if len(parts) == 1:
if await user_exists_cached():
return await handler(event, data)
logger.info(f"[DirectStartBlocker] Прямой старт запрещён для нового пользователя {tg_id}")
return
start_param = parts[1].strip()
if not start_param or not start_param.startswith(self.allowed_prefixes):
if await user_exists_cached():
return await handler(event, data)
logger.info(f"[DirectStartBlocker] Отклонена неизвестная ссылка от {tg_id}: {start_param!r}")
return
async with async_session_maker() as session:
if start_param.startswith("coupons_"):
code = start_param.removeprefix("coupons_")
cached = _cache_coupon.get(code)
if cached and cached[0] > now:
ok = cached[1]
else:
ok = (
await session.execute(select(Coupon.id).where(Coupon.code == code).limit(1))
).first() is not None
_cache_coupon[code] = (now + _TTL, ok)
if not ok:
logger.info(f"[DirectStartBlocker] Купон не найден: {code!r}")
return
elif start_param.startswith("gift_"):
gift_id = start_param.removeprefix("gift_")
cached = _cache_gift.get(gift_id)
if cached and cached[0] > now:
ok = cached[1]
else:
ok = (
await session.execute(
select(Gift.id)
.where(
Gift.gift_id == gift_id,
Gift.is_used.is_(False),
(Gift.expiry_time.is_(None)) | (Gift.expiry_time > datetime.utcnow()),
)
.limit(1)
)
).first() is not None
_cache_gift[gift_id] = (now + _TTL, ok)
if not ok:
logger.info(f"[DirectStartBlocker] Подарок неактивен или не найден: {gift_id!r}")
return
elif start_param.startswith("referral_"):
try:
ref_id = int(start_param.removeprefix("referral_"))
except ValueError:
logger.info(f"[DirectStartBlocker] Неверный формат referral-ссылки: {start_param!r}")
return
cached = _cache_ref.get(ref_id)
if cached and cached[0] > now:
ok = cached[1]
else:
ok = (
await session.execute(select(User.tg_id).where(User.tg_id == ref_id).limit(1))
).first() is not None
_cache_ref[ref_id] = (now + _TTL, ok)
if not ok:
logger.info(f"[DirectStartBlocker] Реферал не найден: {ref_id!r}")
return
elif start_param.startswith("utm"):
utm_code = start_param
cached = _cache_utm.get(utm_code)
if cached and cached[0] > now:
ok = cached[1]
else:
ok = (
await session.execute(
select(TrackingSource.code).where(TrackingSource.code == utm_code).limit(1)
)
).first() is not None
_cache_utm[utm_code] = (now + _TTL, ok)
if not ok:
logger.info(f"[DirectStartBlocker] UTM не найден: {utm_code!r}")
return
logger.info(f"[DirectStartBlocker] Разрешённая и валидная ссылка от {tg_id}: {start_param!r}")
return await handler(event, data)
+4 -9
View File
@@ -1,19 +1,14 @@
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from config import DATABASE_URL
class SessionMiddleware(BaseMiddleware): class SessionMiddleware(BaseMiddleware):
def __init__(self, sessionmaker=None) -> None: def __init__(self, sessionmaker) -> None:
super().__init__() super().__init__()
if sessionmaker is None: self.sessionmaker = sessionmaker
engine = create_async_engine(DATABASE_URL, pool_size=20, max_overflow=0)
self.sessionmaker = async_sessionmaker(engine, expire_on_commit=False)
else:
self.sessionmaker = sessionmaker
async def __call__(self, handler, event, data): async def __call__(self, handler, event, data):
if data.get("session"):
return await handler(event, data)
async with self.sessionmaker() as session: async with self.sessionmaker() as session:
data["session"] = session data["session"] = session
return await handler(event, data) return await handler(event, data)
+31 -20
View File
@@ -1,4 +1,5 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from time import monotonic
from typing import Any from typing import Any
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
@@ -9,10 +10,9 @@ from logger import logger
class UserMiddleware(BaseMiddleware): class UserMiddleware(BaseMiddleware):
""" def __init__(self, debounce_sec: float = 60.0) -> None:
Middleware для обработки информации о пользователе. self._debounce = float(debounce_sec)
Сохраняет или обновляет данные пользователя в базе данных. self._cache: dict[int, tuple[str, float, dict | None]] = {}
"""
async def __call__( async def __call__(
self, self,
@@ -21,30 +21,30 @@ class UserMiddleware(BaseMiddleware):
data: dict[str, Any], data: dict[str, Any],
) -> Any: ) -> Any:
try: try:
if user := data.get("event_from_user"): user: User | None = data.get("event_from_user")
if user and not user.is_bot:
session = data.get("session") session = data.get("session")
db_user = await self._process_user(user, session) db_user = await self._process_user(user, session)
if db_user: if db_user:
data["user"] = db_user data["user"] = db_user
except Exception as e: except Exception as e:
logger.error(f"Ошибка при обработке пользователя: {e}") logger.error(f"Ошибка при обработке пользователя: {e}")
return await handler(event, data) return await handler(event, data)
async def _process_user(self, user: User, session: Any = None) -> dict: async def _process_user(self, user: User, session: Any = None) -> dict | None:
""" uid = user.id
Обрабатывает информацию о пользователе и сохраняет её в базу данных. fp = self._fingerprint(user)
now = monotonic()
Args: cached = self._cache.get(uid)
user (User): Объект пользователя Telegram if cached:
session (Any, optional): Сессия базы данных, если доступна cached_fp, ts, cached_db_user = cached
if fp == cached_fp and now - ts < self._debounce:
return cached_db_user
Returns: logger.debug(f"Обработка пользователя: {uid}")
dict: Словарь с информацией о пользователе из базы данных db_user = await upsert_user(
""" tg_id=uid,
logger.debug(f"Обработка пользователя: {user.id}")
user_data = await upsert_user(
tg_id=user.id,
username=user.username, username=user.username,
first_name=user.first_name, first_name=user.first_name,
last_name=user.last_name, last_name=user.last_name,
@@ -53,6 +53,17 @@ class UserMiddleware(BaseMiddleware):
session=session, session=session,
only_if_exists=True, only_if_exists=True,
) )
self._cache[uid] = (fp, now, db_user)
if db_user:
logger.debug(f"Получены данные пользователя из БД: {uid}")
return db_user
logger.debug(f"Получены данные пользователя из БД: {user.id}") def _fingerprint(self, user: User) -> str:
return user_data return "|".join([
str(user.id),
user.username or "",
user.first_name or "",
user.last_name or "",
user.language_code or "",
"1" if user.is_bot else "0",
])
Binary file not shown.
+10 -5
View File
@@ -47,10 +47,10 @@ def load_module_webhooks(folder: str = "modules") -> list[dict]:
if isinstance(webhook_data, dict) and "path" in webhook_data and "handler" in webhook_data: if isinstance(webhook_data, dict) and "path" in webhook_data and "handler" in webhook_data:
webhooks.append(webhook_data) webhooks.append(webhook_data)
logger.info(f"[Modules] Найден вебхук в модуле {name}: {webhook_data['path']}") logger.info(f"[Modules] Найден вебхук в модуле {name}: {webhook_data['path']}")
except Exception as e: except Exception as e:
logger.error(f"[Modules] Ошибка при загрузке вебхуков из {module_path}: {e}") logger.error(f"[Modules] Ошибка при загрузке вебхуков из {module_path}: {e}")
return webhooks return webhooks
@@ -68,15 +68,20 @@ def load_module_fast_flow_handlers(folder: str = "modules") -> dict:
router_module = importlib.import_module(f"{module_path}.router") router_module = importlib.import_module(f"{module_path}.router")
if hasattr(router_module, "get_fast_flow_handler"): if hasattr(router_module, "get_fast_flow_handler"):
fast_flow_data = router_module.get_fast_flow_handler() fast_flow_data = router_module.get_fast_flow_handler()
if fast_flow_data and isinstance(fast_flow_data, dict) and "payment_key" in fast_flow_data and "handler" in fast_flow_data: if (
fast_flow_data
and isinstance(fast_flow_data, dict)
and "payment_key" in fast_flow_data
and "handler" in fast_flow_data
):
payment_key = fast_flow_data["payment_key"] payment_key = fast_flow_data["payment_key"]
handler = fast_flow_data["handler"] handler = fast_flow_data["handler"]
handlers[payment_key] = handler handlers[payment_key] = handler
logger.info(f"[Modules] Найден обработчик быстрого флоу в модуле {name}: {payment_key}") logger.info(f"[Modules] Найден обработчик быстрого флоу в модуле {name}: {payment_key}")
elif fast_flow_data is None: elif fast_flow_data is None:
logger.info(f"[Modules] Быстрое флоу отключено в модуле {name}") logger.info(f"[Modules] Быстрое флоу отключено в модуле {name}")
except Exception as e: except Exception as e:
logger.error(f"[Modules] Ошибка при загрузке быстрого флоу из {module_path}: {e}") logger.error(f"[Modules] Ошибка при загрузке быстрого флоу из {module_path}: {e}")
return handlers return handlers
+1 -1
View File
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
def get_version() -> str: def get_version() -> str:
return f"v.5 - preview {get_git_commit_number()}" return f"v.5-150949 {get_git_commit_number()}"

Some files were not shown because too many files have changed in this diff Show More