currency/fast flow for everyone/optimization and cache/new payment system/and more
This commit is contained in:
@@ -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,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,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))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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"))
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
|
||||||
@@ -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)
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"))
|
||||||
+365
-405
@@ -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=[]),
|
|
||||||
)
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
@@ -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=[]),
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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/"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ("router",)
|
||||||
|
|
||||||
|
from .handlers import router
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+29
-9
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user