feat(referral): add referral system with withdrawals

This commit is contained in:
2026-07-18 10:58:14 +05:00
parent 5e5bab91f2
commit a04734b8a7
17 changed files with 1750 additions and 60 deletions
+27 -1
View File
@@ -6,7 +6,29 @@ from .db_purchases import PurchaseModel, Purchasesx
from .db_refill import RefillModel, Refillx
from .db_settings import SettingsModel, Settingsx
from .db_users import UserModel, UsersRepository, Userx
from .entities import Category, Item, Payments, Position, Purchase, Refill, Settings, User
from .db_referral import (
ReferralModel,
ReferralTransactionModel,
ReferralWithdrawalModel,
Referralx,
ReferralTransactionx,
ReferralWithdrawalx,
)
from .entities import (
Category,
Item,
Payments,
Position,
Purchase,
Refill,
Settings,
User,
Referral,
ReferralTransaction,
ReferralWithdrawal,
)
ModelCategory = Category
ModelItem = Item
@@ -18,3 +40,7 @@ ModelSettings = Settings
ModelUser = User
ModelUsers = User
SettingsRepository = Settingsx
ModelReferral = Referral
ModelReferralTransaction = ReferralTransaction
ModelReferralWithdrawal = ReferralWithdrawal
+728
View File
@@ -0,0 +1,728 @@
# - *- coding: utf- 8 - *-
from math import isfinite
import re
from urllib.parse import urlparse
from uuid import uuid4
from sqlalchemy import BigInteger, Float, Integer, String, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncConnection
from typing import Any, Dict, Optional, Tuple, Union
from tgbot.database.core import Base, session_factory
from tgbot.database.entities import Referral, ReferralTransaction, ReferralWithdrawal
from tgbot.database.repository import BaseRepository
from tgbot.utils.const_functions import get_unix
def is_valid_referral_withdrawal_recipient(
withdrawal_method: str, recipient: str
) -> bool:
if withdrawal_method == "Cryptobot":
return bool(re.fullmatch(r"@[A-Za-z][A-Za-z0-9_]{4,31}", recipient))
if withdrawal_method == "Lolzteam":
parsed_url = urlparse(recipient)
return parsed_url.scheme in ("http", "https") and parsed_url.hostname in (
"zelenka.guru",
"lolz.team",
"lolz.live",
"lolz.guru",
)
return False
async def ensure_referrals_schema(conn: AsyncConnection) -> None:
user_columns_result = await conn.exec_driver_sql("PRAGMA table_info(storage_users)")
user_columns = {column[1] for column in user_columns_result.fetchall()}
# Обновление полей пользователя
if user_columns:
if "user_referrer_id" not in user_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_users ADD COLUMN user_referrer_id BIGINT"
)
if "user_referral_balance" not in user_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_users "
"ADD COLUMN user_referral_balance FLOAT NOT NULL DEFAULT 0"
)
if "user_referral_hold" not in user_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_users "
"ADD COLUMN user_referral_hold FLOAT NOT NULL DEFAULT 0"
)
await conn.exec_driver_sql(
"CREATE INDEX IF NOT EXISTS ix_storage_users_user_referrer_id "
"ON storage_users (user_referrer_id)"
)
settings_columns_result = await conn.exec_driver_sql(
"PRAGMA table_info(storage_settings)"
)
settings_columns = {column[1] for column in settings_columns_result.fetchall()}
# Обновление полей настроек
if settings_columns:
if "status_referral" not in settings_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_settings "
"ADD COLUMN status_referral VARCHAR(16) NOT NULL DEFAULT 'False'"
)
if "referral_bonus_rub" not in settings_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_settings "
"ADD COLUMN referral_bonus_rub FLOAT NOT NULL DEFAULT 0"
)
if "referral_refill_percent" not in settings_columns:
await conn.exec_driver_sql(
"ALTER TABLE storage_settings "
"ADD COLUMN referral_refill_percent FLOAT NOT NULL DEFAULT 0"
)
class ReferralModel(Base):
__tablename__ = "storage_referrals"
increment: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
referrer_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
referral_id: Mapped[int] = mapped_column(
BigInteger, nullable=False, unique=True, index=True
)
referral_unix: Mapped[int] = mapped_column(
Integer, nullable=False, default=get_unix
)
class ReferralTransactionModel(Base):
__tablename__ = "storage_referral_transactions"
__table_args__ = (
UniqueConstraint(
"user_id",
"transaction_type",
"source_id",
name="uq_referral_transaction_source",
),
)
increment: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
related_user_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
transaction_type: Mapped[str] = mapped_column(String(64), nullable=False)
source_id: Mapped[str] = mapped_column(String(128), nullable=False)
amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
transaction_unix: Mapped[int] = mapped_column(
Integer, nullable=False, default=get_unix
)
class ReferralWithdrawalModel(Base):
__tablename__ = "storage_referral_withdrawals"
increment: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
withdrawal_amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
withdrawal_method: Mapped[str] = mapped_column(String(32), nullable=False)
withdrawal_recipient: Mapped[str] = mapped_column(String(255), nullable=False)
withdrawal_status: Mapped[str] = mapped_column(
String(32), nullable=False, default="pending"
)
withdrawal_unix: Mapped[int] = mapped_column(
Integer, nullable=False, default=get_unix
)
processed_unix: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
processed_admin_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
class Referralx(BaseRepository[ReferralModel, Referral]):
def __init__(self):
super().__init__()
self.table_model = ReferralModel
self.entity_model = Referral
self.storage_name = ReferralModel.__tablename__
async def add(self, referrer_id: int, referral_id: int) -> Referral:
return await self._insert(
referrer_id=referrer_id,
referral_id=referral_id,
referral_unix=get_unix(),
)
async def register_referral(self, referral_id: int, referrer_id: int) -> str:
if referral_id == referrer_id:
return "SELF_REFERRAL"
async with session_factory() as session:
try:
await session.execute(text("BEGIN IMMEDIATE"))
settings_result = await session.execute(
text(
"SELECT status_referral, referral_bonus_rub "
"FROM storage_settings WHERE id = 1"
)
)
settings = settings_result.mappings().first()
if settings is None or settings["status_referral"] != "True":
await session.rollback()
return "DISABLED"
referrer_result = await session.execute(
text(
"SELECT user_referral_balance FROM storage_users "
"WHERE user_id = :user_id"
),
{"user_id": referrer_id},
)
referrer = referrer_result.mappings().first()
if referrer is None:
await session.rollback()
return "REFERRER_NOT_FOUND"
referral_result = await session.execute(
text(
"SELECT user_balance, user_referrer_id "
"FROM storage_users WHERE user_id = :user_id"
),
{"user_id": referral_id},
)
referral = referral_result.mappings().first()
if referral is None:
await session.rollback()
return "REFERRAL_NOT_FOUND"
existing_referral_result = await session.execute(
text(
"SELECT increment FROM storage_referrals "
"WHERE referral_id = :user_id LIMIT 1"
),
{"user_id": referral_id},
)
if (
referral["user_referrer_id"] is not None
or existing_referral_result.mappings().first() is not None
):
await session.rollback()
return "ALREADY"
bonus = round(float(settings["referral_bonus_rub"]), 2)
source_id = f"registration:{referral_id}"
now_unix = get_unix()
# Приглашённому — бонус на основной баланс.
await session.execute(
text(
"INSERT INTO storage_referrals "
"(referrer_id, referral_id, referral_unix) "
"VALUES (:referrer_id, :referral_id, :referral_unix)"
),
{
"referrer_id": referrer_id,
"referral_id": referral_id,
"referral_unix": now_unix,
},
)
await session.execute(
text(
"UPDATE storage_users "
"SET user_referrer_id = :referrer_id, "
"user_balance = :balance "
"WHERE user_id = :user_id"
),
{
"referrer_id": referrer_id,
"balance": round(float(referral["user_balance"]) + bonus, 2),
"user_id": referral_id,
},
)
# Рефереру — бонус на реферальный баланс.
await session.execute(
text(
"UPDATE storage_users "
"SET user_referral_balance = :balance "
"WHERE user_id = :user_id"
),
{
"balance": round(
float(referrer["user_referral_balance"]) + bonus, 2
),
"user_id": referrer_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
),
{
"user_id": referral_id,
"related_user_id": referrer_id,
"transaction_type": "registration_bonus_main",
"source_id": source_id,
"amount": bonus,
"transaction_unix": now_unix,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
),
{
"user_id": referrer_id,
"related_user_id": referral_id,
"transaction_type": "registration_bonus_referral",
"source_id": source_id,
"amount": bonus,
"transaction_unix": now_unix,
},
)
await session.commit()
except Exception:
await session.rollback()
raise
return "ok"
async def transfer_to_main_balance(self, user_id: int, amount: float) -> str:
try:
transfer_amount = round(float(amount), 2)
except (TypeError, ValueError):
return "INVALID_AMOUNT"
if not isfinite(transfer_amount) or transfer_amount <= 0:
return "INVALID_AMOUNT"
async with session_factory() as session:
try:
await session.execute(text("BEGIN IMMEDIATE"))
user_result = await session.execute(
text(
"SELECT user_balance, user_referral_balance, user_referral_hold "
"FROM storage_users WHERE user_id = :user_id"
),
{"user_id": user_id},
)
get_user = user_result.mappings().first()
if get_user is None:
await session.rollback()
return "USER_NOT_FOUND"
available_balance = round(
float(get_user["user_referral_balance"])
- float(get_user["user_referral_hold"]),
2,
)
if transfer_amount > available_balance:
await session.rollback()
return "INSUFFICIENT_FUNDS"
await session.execute(
text(
"UPDATE storage_users "
"SET user_balance = :user_balance, "
"user_referral_balance = :referral_balance "
"WHERE user_id = :user_id"
),
{
"user_balance": round(
float(get_user["user_balance"]) + transfer_amount,
2,
),
"referral_balance": round(
float(get_user["user_referral_balance"]) - transfer_amount,
2,
),
"user_id": user_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :transaction_unix)"
),
{
"user_id": user_id,
"transaction_type": "transfer_to_main",
"source_id": f"transfer:{user_id}:{uuid4().hex}",
"amount": transfer_amount,
"transaction_unix": get_unix(),
},
)
await session.commit()
except Exception:
await session.rollback()
raise
return "ok"
class ReferralTransactionx(
BaseRepository[ReferralTransactionModel, ReferralTransaction]
):
def __init__(self):
super().__init__()
self.table_model = ReferralTransactionModel
self.entity_model = ReferralTransaction
self.storage_name = ReferralTransactionModel.__tablename__
async def get_refill_reward(self, source_id: str) -> Optional[ReferralTransaction]:
return await self.get(
transaction_type="refill_percent",
source_id=source_id,
)
async def add(
self,
user_id: int,
transaction_type: str,
source_id: str,
amount: float,
related_user_id: Optional[int] = None,
) -> ReferralTransaction:
return await self._insert(
user_id=user_id,
related_user_id=related_user_id,
transaction_type=transaction_type,
source_id=source_id,
amount=round(amount, 2),
transaction_unix=get_unix(),
)
class ReferralWithdrawalx(BaseRepository[ReferralWithdrawalModel, ReferralWithdrawal]):
def __init__(self):
super().__init__()
self.table_model = ReferralWithdrawalModel
self.entity_model = ReferralWithdrawal
self.storage_name = ReferralWithdrawalModel.__tablename__
async def create_pending(
self,
user_id: int,
withdrawal_amount: float,
withdrawal_method: str,
withdrawal_recipient: str,
) -> Tuple[str, Optional[ReferralWithdrawal]]:
try:
amount = round(float(withdrawal_amount), 2)
except (TypeError, ValueError):
return "INVALID_AMOUNT", None
recipient = str(withdrawal_recipient).strip()
if not isfinite(amount) or amount <= 0:
return "INVALID_AMOUNT", None
if withdrawal_method not in ("Cryptobot", "Lolzteam"):
return "INVALID_METHOD", None
if not is_valid_referral_withdrawal_recipient(withdrawal_method, recipient):
return "INVALID_RECIPIENT", None
withdrawal_id: Optional[int] = None
async with session_factory() as session:
try:
await session.execute(text("BEGIN IMMEDIATE"))
user_result = await session.execute(
text(
"SELECT user_referral_balance, user_referral_hold "
"FROM storage_users WHERE user_id = :user_id"
),
{"user_id": user_id},
)
get_user = user_result.mappings().first()
if get_user is None:
await session.rollback()
return "USER_NOT_FOUND", None
available_balance = round(
float(get_user["user_referral_balance"])
- float(get_user["user_referral_hold"]),
2,
)
if amount > available_balance:
await session.rollback()
return "INSUFFICIENT_FUNDS", None
now_unix = get_unix()
withdrawal_result = await session.execute(
text(
"INSERT INTO storage_referral_withdrawals "
"(user_id, withdrawal_amount, withdrawal_method, withdrawal_recipient, withdrawal_status, withdrawal_unix) "
"VALUES (:user_id, :amount, :method, :recipient, 'pending', :unix)"
),
{
"user_id": user_id,
"amount": amount,
"method": withdrawal_method,
"recipient": recipient,
"unix": now_unix,
},
)
withdrawal_id = int(withdrawal_result.lastrowid)
await session.execute(
text(
"UPDATE storage_users "
"SET user_referral_hold = :hold "
"WHERE user_id = :user_id"
),
{
"hold": round(
float(get_user["user_referral_hold"]) + amount,
2,
),
"user_id": user_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
),
{
"user_id": user_id,
"transaction_type": "withdrawal_hold",
"source_id": f"withdrawal:{withdrawal_id}",
"amount": amount,
"unix": now_unix,
},
)
await session.commit()
except Exception:
await session.rollback()
raise
return "ok", await self.get(increment=withdrawal_id)
async def complete(
self, withdrawal_id: int, admin_id: int
) -> Tuple[str, Optional[ReferralWithdrawal]]:
async with session_factory() as session:
try:
await session.execute(text("BEGIN IMMEDIATE"))
withdrawal_result = await session.execute(
text(
"SELECT user_id, withdrawal_amount, withdrawal_status "
"FROM storage_referral_withdrawals WHERE increment = :withdrawal_id"
),
{"withdrawal_id": withdrawal_id},
)
withdrawal = withdrawal_result.mappings().first()
if withdrawal is None:
await session.rollback()
return "NOT_FOUND", None
if withdrawal["withdrawal_status"] != "pending":
await session.rollback()
return "ALREADY_PROCESSED", None
user_result = await session.execute(
text(
"SELECT user_referral_balance, user_referral_hold "
"FROM storage_users WHERE user_id = :user_id"
),
{"user_id": withdrawal["user_id"]},
)
get_user = user_result.mappings().first()
amount = float(withdrawal["withdrawal_amount"])
if (
get_user is None
or float(get_user["user_referral_balance"]) < amount
or float(get_user["user_referral_hold"]) < amount
):
await session.rollback()
return "BALANCE_ERROR", None
now_unix = get_unix()
await session.execute(
text(
"UPDATE storage_users "
"SET user_referral_balance = :balance, user_referral_hold = :hold "
"WHERE user_id = :user_id"
),
{
"balance": round(
float(get_user["user_referral_balance"]) - amount,
2,
),
"hold": round(
float(get_user["user_referral_hold"]) - amount,
2,
),
"user_id": withdrawal["user_id"],
},
)
await session.execute(
text(
"UPDATE storage_referral_withdrawals "
"SET withdrawal_status = 'completed', processed_unix = :unix, processed_admin_id = :admin_id "
"WHERE increment = :withdrawal_id"
),
{
"unix": now_unix,
"admin_id": admin_id,
"withdrawal_id": withdrawal_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
),
{
"user_id": withdrawal["user_id"],
"transaction_type": "withdrawal_completed",
"source_id": f"withdrawal:{withdrawal_id}",
"amount": amount,
"unix": now_unix,
},
)
await session.commit()
except Exception:
await session.rollback()
raise
return "ok", await self.get(increment=withdrawal_id)
async def reject(
self, withdrawal_id: int, admin_id: int
) -> Tuple[str, Optional[ReferralWithdrawal]]:
async with session_factory() as session:
try:
await session.execute(text("BEGIN IMMEDIATE"))
withdrawal_result = await session.execute(
text(
"SELECT user_id, withdrawal_amount, withdrawal_status "
"FROM storage_referral_withdrawals WHERE increment = :withdrawal_id"
),
{"withdrawal_id": withdrawal_id},
)
withdrawal = withdrawal_result.mappings().first()
if withdrawal is None:
await session.rollback()
return "NOT_FOUND", None
if withdrawal["withdrawal_status"] != "pending":
await session.rollback()
return "ALREADY_PROCESSED", None
user_result = await session.execute(
text(
"SELECT user_referral_hold FROM storage_users "
"WHERE user_id = :user_id"
),
{"user_id": withdrawal["user_id"]},
)
get_user = user_result.mappings().first()
amount = float(withdrawal["withdrawal_amount"])
if get_user is None or float(get_user["user_referral_hold"]) < amount:
await session.rollback()
return "BALANCE_ERROR", None
now_unix = get_unix()
await session.execute(
text(
"UPDATE storage_users "
"SET user_referral_hold = :hold WHERE user_id = :user_id"
),
{
"hold": round(
float(get_user["user_referral_hold"]) - amount,
2,
),
"user_id": withdrawal["user_id"],
},
)
await session.execute(
text(
"UPDATE storage_referral_withdrawals "
"SET withdrawal_status = 'rejected', processed_unix = :unix, processed_admin_id = :admin_id "
"WHERE increment = :withdrawal_id"
),
{
"unix": now_unix,
"admin_id": admin_id,
"withdrawal_id": withdrawal_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
),
{
"user_id": withdrawal["user_id"],
"transaction_type": "withdrawal_rejected",
"source_id": f"withdrawal:{withdrawal_id}",
"amount": amount,
"unix": now_unix,
},
)
await session.commit()
except Exception:
await session.rollback()
raise
return "ok", await self.get(increment=withdrawal_id)
async def add(
self,
user_id: int,
withdrawal_amount: float,
withdrawal_method: str,
withdrawal_recipient: str,
) -> ReferralWithdrawal:
return await self._insert(
user_id=user_id,
withdrawal_amount=round(withdrawal_amount, 2),
withdrawal_method=withdrawal_method,
withdrawal_recipient=withdrawal_recipient,
withdrawal_status="pending",
withdrawal_unix=get_unix(),
)
async def update(
self,
where: Optional[Union[Dict[str, Any], int]] = None,
**kwargs,
) -> int:
if isinstance(where, int):
where = {"increment": where}
return await self._update(where=where, **kwargs)
+69 -1
View File
@@ -90,7 +90,7 @@ class Refillx(BaseRepository[RefillModel, Refill]):
user_result = await session.execute(
text(
"""
SELECT user_balance, user_refill
SELECT user_balance, user_refill, user_referrer_id
FROM storage_users
WHERE user_id = :user_id
"""
@@ -103,6 +103,14 @@ class Refillx(BaseRepository[RefillModel, Refill]):
await session.rollback()
return "USER_NOT_FOUND"
settings_result = await session.execute(
text(
"SELECT status_referral, referral_refill_percent "
"FROM storage_settings WHERE id = 1"
)
)
get_settings = settings_result.mappings().first()
new_balance = round(float(get_user["user_balance"]) + float(pay_amount), 2)
new_refill = round(float(get_user["user_refill"]) + float(pay_amount), 2)
@@ -138,6 +146,66 @@ class Refillx(BaseRepository[RefillModel, Refill]):
),
{"balance": new_balance, "refill": new_refill, "user_id": user_id},
)
referrer_id = get_user["user_referrer_id"]
referral_reward = 0.0
if (
get_settings is not None
and get_settings["status_referral"] == "True"
and referrer_id is not None
and referrer_id != user_id
):
referral_reward = round(
float(pay_amount)
* float(get_settings["referral_refill_percent"])
/ 100,
2,
)
if referral_reward > 0:
referrer_result = await session.execute(
text(
"SELECT user_referral_balance FROM storage_users "
"WHERE user_id = :user_id"
),
{"user_id": referrer_id},
)
get_referrer = referrer_result.mappings().first()
if get_referrer is not None:
source_id = f"refill:{pay_comment or pay_receipt}"
await session.execute(
text(
"UPDATE storage_users "
"SET user_referral_balance = :balance "
"WHERE user_id = :user_id"
),
{
"balance": round(
float(get_referrer["user_referral_balance"])
+ referral_reward,
2,
),
"user_id": referrer_id,
},
)
await session.execute(
text(
"INSERT INTO storage_referral_transactions "
"(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
"VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
),
{
"user_id": referrer_id,
"related_user_id": user_id,
"transaction_type": "refill_percent",
"source_id": source_id,
"amount": referral_reward,
"transaction_unix": get_unix(),
},
)
await session.commit()
except Exception:
await session.rollback()
+10 -1
View File
@@ -1,7 +1,7 @@
# - *- coding: utf- 8 - *-
from typing import Any, Dict, Optional
from sqlalchemy import Integer, String
from sqlalchemy import Integer, String, Float
from sqlalchemy.orm import Mapped, mapped_column
from tgbot.database.core import Base
@@ -46,6 +46,15 @@ class SettingsModel(Base):
misc_profit_week: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
misc_profit_month: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# Реферальная система
referral_refill_percent: Mapped[float] = mapped_column(
Float, nullable=False, default=0
)
status_referral: Mapped[str] = mapped_column(
String(16), nullable=False, default="False"
)
referral_bonus_rub: Mapped[float] = mapped_column(Float, nullable=False, default=0)
ModelBase = Settings
BaseModel = Settings
+33 -17
View File
@@ -15,8 +15,12 @@ from tgbot.utils.const_functions import get_unix
class UserModel(Base):
__tablename__ = "storage_users"
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
increment: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
user_id: Mapped[int] = mapped_column(
BigInteger, nullable=False, unique=True, index=True
)
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
@@ -26,6 +30,13 @@ class UserModel(Base):
user_give: Mapped[float] = mapped_column(Float, nullable=False, default=0)
user_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
# Поля для реферальной системы
user_referrer_id: Mapped[int] = mapped_column(BigInteger, nullable=True, index=True)
user_referral_balance: Mapped[float] = mapped_column(
Float, nullable=False, default=0
)
user_referral_hold: Mapped[float] = mapped_column(Float, nullable=False, default=0)
ModelBase = User
BaseModel = User
@@ -41,31 +52,34 @@ class UsersRepository(BaseRepository[UserModel, User]):
# Добавление или обновление пользователя
async def add(
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str = "",
user_fullname: str = "",
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str = "",
user_fullname: str = "",
) -> User:
return await self.upsert(
user_id=user_id,
user_login=user_login,
user_name=user_name,
user_surname=user_surname,
user_fullname=user_fullname or " ".join(filter(None, [user_name, user_surname])),
user_fullname=user_fullname
or " ".join(filter(None, [user_name, user_surname])),
)
# Выполнение upsert пользователя по телеграм ID
async def upsert(
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str = "",
user_fullname: str = "",
self,
user_id: int,
user_login: str,
user_name: str,
user_surname: str = "",
user_fullname: str = "",
) -> User:
user_fullname = user_fullname or " ".join(filter(None, [user_name, user_surname]))
user_fullname = user_fullname or " ".join(
filter(None, [user_name, user_surname])
)
user_table = UserModel.__table__
statement = insert(UserModel).values(
user_id=user_id,
@@ -105,7 +119,9 @@ class UsersRepository(BaseRepository[UserModel, User]):
return user
# Обновление пользователя по ID или фильтру
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
async def update(
self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs
) -> int:
if isinstance(where, int):
where = {"user_id": where}
+42
View File
@@ -97,6 +97,11 @@ class Settings:
misc_profit_week: int
misc_profit_month: int
# Реферальная система
referral_refill_percent: float
status_referral: str
referral_bonus_rub: float
@dataclass
class User:
@@ -110,3 +115,40 @@ class User:
user_refill: float
user_give: float
user_unix: int
# Поля для реферальной системы
user_referrer_id: Optional[int]
user_referral_balance: float
user_referral_hold: float
@dataclass
class Referral:
increment: int
referrer_id: int
referral_id: int
referral_unix: int
@dataclass
class ReferralTransaction:
increment: int
user_id: int
related_user_id: Optional[int]
transaction_type: str
source_id: str
amount: float
transaction_unix: int
@dataclass
class ReferralWithdrawal:
increment: int
user_id: int
withdrawal_amount: float
withdrawal_method: str
withdrawal_recipient: str
withdrawal_status: str
withdrawal_unix: int
processed_unix: Optional[int]
processed_admin_id: Optional[int]
+3
View File
@@ -160,10 +160,13 @@ async def prepare_database() -> None:
import tgbot.database.db_refill # noqa: F401
import tgbot.database.db_settings # noqa: F401
import tgbot.database.db_users # noqa: F401
import tgbot.database.db_referral # noqa: F401
async with engine.begin() as conn:
from tgbot.database.db_payments import ensure_payments_schema
from tgbot.database.db_referral import ensure_referrals_schema
await ensure_referrals_schema(conn)
await ensure_payments_schema(conn)
await conn.run_sync(Base.metadata.create_all)
+34
View File
@@ -20,6 +20,17 @@ def close_finl() -> InlineKeyboardMarkup:
# Рассылка
def referral_withdrawal_actions_finl(withdrawal_id: int) -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.row(
ikb("✅ Выполнено", data=f"referral_withdrawal_complete:{withdrawal_id}"),
ikb("❌ Отклонить", data=f"referral_withdrawal_reject:{withdrawal_id}"),
)
return keyboard.as_markup()
def mail_confirm_finl() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
@@ -207,6 +218,17 @@ async def settings_finl() -> InlineKeyboardMarkup:
else:
method_prod_kb = ikb("На каждой строке", data="settings_edit_method_prod:skip")
# Бонус за приглашение
referral_bonus_kb = ikb(
f"{get_settings.referral_bonus_rub}", data="settings_edit_referral_bonus"
)
# Доход с пополнений
deposit_percent_kb = ikb(
f"{get_settings.referral_refill_percent}%",
data="settings_edit_referral_percent",
)
keyboard.row(
ikb("❔ FAQ", data="..."),
faq_kb,
@@ -222,6 +244,12 @@ async def settings_finl() -> InlineKeyboardMarkup:
).row(
ikb("🎁 Метод добавления", data="..."),
method_prod_kb,
).row(
ikb("🤝 Бонус за приглашение", data="..."),
referral_bonus_kb,
).row(
ikb("📈 Доход с пополнений", data="..."),
deposit_percent_kb,
)
return keyboard.as_markup()
@@ -236,6 +264,7 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
status_work_kb = ikb("Включены ✅", data="settings_status_work:False")
status_buy_kb = ikb("Включены ✅", data="settings_status_buy:False")
status_refill_kb = ikb("Включены ✅", data="settings_status_refill:False")
status_referral_kb = ikb("Включена ✅", data="settings_status_referral:False")
notification_buy_kb = ikb("Включены 🔔", data="settings_notification_buy:False")
notification_refill_kb = ikb(
"Включены 🔔", data="settings_notification_refill:False"
@@ -247,6 +276,8 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
status_work_kb = ikb("Выключены ❌", data="settings_status_work:True")
if get_settings.status_refill == "False":
status_refill_kb = ikb("Выключены ❌", data="settings_status_refill:True")
if get_settings.status_referral == "False":
status_referral_kb = ikb("Выключена ❌", data="settings_status_referral:True")
if get_settings.notification_buy == "False":
notification_buy_kb = ikb("Выключены 🔕", data="settings_notification_buy:True")
if get_settings.notification_refill == "False":
@@ -263,6 +294,9 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
).row(
ikb("🎁 Покупки", data="..."),
status_buy_kb,
).row(
ikb("🤝 Реферальная система", data="..."),
status_referral_kb,
).row(
ikb("📢 Увед. о покупках", data="..."),
notification_buy_kb,
+35
View File
@@ -33,6 +33,41 @@ def user_support_finl(support_login: str) -> InlineKeyboardMarkup:
return keyboard.as_markup()
def referral_menu_finl(referral_link: str) -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.row(
ikb("💸 Перевести на основной баланс", data="referral_transfer"),
).row(
ikb("📥 Вывести средства", data="referral_withdrawal"),
)
keyboard.row(ikb("▪️ Скопировать ссылку", copy=referral_link))
return keyboard.as_markup()
def referral_transfer_method_finl() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.row(ikb("🔙 Вернуться", data="referral_menu"))
return keyboard.as_markup()
def referral_withdrawal_method_finl() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
keyboard.row(
ikb("🔷 CryptoBot", data="referral_withdrawal_method:Cryptobot"),
).row(
ikb("🟢 Lolzteam", data="referral_withdrawal_method:Lolzteam"),
).row(
ikb("🔙 Вернуться", data="referral_menu"),
)
return keyboard.as_markup()
################################################################################
################################### ПЛАТЕЖИ ####################################
# Выбор способа пополнения
+2
View File
@@ -14,6 +14,8 @@ def menu_frep(user_id: int) -> ReplyKeyboardMarkup:
rkb("🎁 Купить"),
rkb("👤 Профиль"),
rkb("🧮 Наличие товаров"),
).row(
rkb("🤝 Реферальная система"),
).row(
rkb("☎️ Поддержка"),
rkb("❔ FAQ"),
+67 -2
View File
@@ -3,16 +3,37 @@ from aiogram import BaseMiddleware
from cachetools import TTLCache
from tgbot.data.config import BOT_USER_CACHE_TTL
from tgbot.database import Userx
from tgbot.database import Referralx, Settingsx, Userx
from tgbot.utils.const_functions import clear_html
from tgbot.utils.misc.bot_logging import bot_logger
class ExistsUserMiddleware(BaseMiddleware):
# Создание кеша пользователей
def __init__(self, cache_ttl: int = BOT_USER_CACHE_TTL) -> None:
self.users = Userx()
self.referrals = Referralx()
self.cache = TTLCache(maxsize=10_000, ttl=cache_ttl)
@staticmethod
def _get_referrer_id(event) -> int | None:
event_text = getattr(event, "text", None)
if not isinstance(event_text, str):
return None
start_data = event_text.split(maxsplit=1)
if len(start_data) != 2 or start_data[0] != "/start":
return None
referral_data = start_data[1]
if not referral_data.startswith("r_") or not referral_data[2:].isdigit():
return None
return int(referral_data[2:])
# Добавление или обновление пользователя перед handler-ом
async def __call__(self, handler, event, data):
this_user = data.get("event_from_user")
@@ -32,6 +53,12 @@ class ExistsUserMiddleware(BaseMiddleware):
)
cached_user = self.cache.get(user_id)
existing_user = None
if cached_user is None:
existing_user = await self.users.get(user_id=user_id)
is_new_user = cached_user is None and existing_user is None
if cached_user is None or cached_user["data"] != user_data:
user = await self.users.upsert(
@@ -41,10 +68,48 @@ class ExistsUserMiddleware(BaseMiddleware):
user_surname=user_data[2],
user_fullname=user_data[3],
)
self.cache[user_id] = {"data": user_data, "user": user}
else:
user = cached_user["user"]
referrer_id = self._get_referrer_id(event)
if is_new_user and referrer_id is not None:
registration_status = await self.referrals.register_referral(
referral_id=user_id,
referrer_id=referrer_id,
)
if registration_status == "ok":
user = await self.users.get_required(user_id=user_id)
get_settings = await Settingsx().get()
bot = data.get("bot") or getattr(event, "bot", None)
if bot is not None:
notifications = (
(
referrer_id,
"<b>🤝 Новый реферал</b>\n\n"
f"▪️ Пользователь: <a href='tg://user?id={user_id}'>{user.user_name}</a>\n"
f"▪️ Начислено: <code>{get_settings.referral_bonus_rub}₽</code>",
),
(
user_id,
"<b>🤝 Реферальный бонус</b>\n\n"
f"▪️ Вам начислено: <code>{get_settings.referral_bonus_rub}₽</code>",
),
)
for recipient_id, notification_text in notifications:
try:
await bot.send_message(recipient_id, notification_text)
except Exception:
bot_logger.warning(
"Не удалось отправить уведомление о реферальной регистрации",
exc_info=True,
)
self.cache[user_id] = {"data": user_data, "user": user}
data["User"] = user
return await handler(event, data)
+46 -12
View File
@@ -2,8 +2,19 @@
from aiogram import Dispatcher
from tgbot.routers import main_errors, main_start, main_missed
from tgbot.routers.admin import admin_menu, admin_functions, admin_payments, admin_products, admin_settings
from tgbot.routers.user import user_menu, user_transactions, user_products
from tgbot.routers.admin import (
admin_menu,
admin_functions,
admin_payments,
admin_products,
admin_settings,
)
from tgbot.routers.user import (
user_menu,
user_referral,
user_transactions,
user_products,
)
from tgbot.utils.misc.bot_filters import IsAdmin, IsPrivate
@@ -23,17 +34,39 @@ def register_all_routers(dp: Dispatcher):
user_products.router.callback_query.filter(IsPrivate())
user_transactions.router.message.filter(IsPrivate())
user_transactions.router.callback_query.filter(IsPrivate())
user_referral.router.message.filter(IsPrivate())
user_referral.router.callback_query.filter(IsPrivate())
admin_menu.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
admin_menu.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
admin_functions.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
admin_functions.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
admin_payments.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
admin_payments.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
admin_settings.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
admin_settings.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
admin_products.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
admin_products.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
admin_menu.router.message.filter(
IsPrivate(), IsAdmin()
) # Работа message роутера только для админов
admin_menu.router.callback_query.filter(
IsPrivate(), IsAdmin()
) # Работа callback роутера только для админов
admin_functions.router.message.filter(
IsPrivate(), IsAdmin()
) # Работа message роутера только для админов
admin_functions.router.callback_query.filter(
IsPrivate(), IsAdmin()
) # Работа callback роутера только для админов
admin_payments.router.message.filter(
IsPrivate(), IsAdmin()
) # Работа message роутера только для админов
admin_payments.router.callback_query.filter(
IsPrivate(), IsAdmin()
) # Работа callback роутера только для админов
admin_settings.router.message.filter(
IsPrivate(), IsAdmin()
) # Работа message роутера только для админов
admin_settings.router.callback_query.filter(
IsPrivate(), IsAdmin()
) # Работа callback роутера только для админов
admin_products.router.message.filter(
IsPrivate(), IsAdmin()
) # Работа message роутера только для админов
admin_products.router.callback_query.filter(
IsPrivate(), IsAdmin()
) # Работа callback роутера только для админов
# Подключение обязательных роутеров
dp.include_router(main_errors.router) # Роутер ошибки
@@ -44,6 +77,7 @@ def register_all_routers(dp: Dispatcher):
dp.include_router(admin_menu.router) # Админ роутер
dp.include_router(user_products.router) # Юзер роутер
dp.include_router(user_transactions.router) # Юзер роутер
dp.include_router(user_referral.router) # Юзер роутер
dp.include_router(admin_functions.router) # Админ роутер
dp.include_router(admin_payments.router) # Админ роутер
dp.include_router(admin_settings.router) # Админ роутер
+175 -23
View File
@@ -5,18 +5,137 @@ from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message
from tgbot.database import Purchasesx, Refillx, Userx
from tgbot.database import Purchasesx, Refillx, ReferralWithdrawalx, Userx
from tgbot.keyboards.inline_admin import profile_edit_return_finl, mail_confirm_finl
from tgbot.services.api_hosting_text import HostingAPI
from tgbot.utils.const_functions import is_number, to_number, del_message, ded, clear_html, convert_date
from tgbot.utils.const_functions import (
is_number,
to_number,
del_message,
ded,
clear_html,
convert_date,
)
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import FSM, ARS
from tgbot.utils.misc_functions import functions_mail_make
from tgbot.utils.text_functions import open_profile_admin, refill_open_admin, purchase_open_admin
from tgbot.utils.text_functions import (
open_profile_admin,
refill_open_admin,
purchase_open_admin,
)
router = Router(name=__name__)
def withdrawal_method_title(withdrawal_method: str) -> str:
if withdrawal_method == "Cryptobot":
return "CryptoBot"
return withdrawal_method
@router.callback_query(F.data.startswith("referral_withdrawal_complete:"))
async def referral_withdrawal_complete(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
withdrawal_id_raw = call.data.split(":", 1)[1]
if not withdrawal_id_raw.isdigit():
return await call.answer("❌ Заявка не найдена", True)
status, withdrawal = await ReferralWithdrawalx().complete(
withdrawal_id=int(withdrawal_id_raw),
admin_id=call.from_user.id,
)
if status == "ALREADY_PROCESSED":
return await call.answer("❌ Заявка уже обработана", True)
if status != "ok" or withdrawal is None:
return await call.answer("❌ Не удалось обработать заявку", True)
method_title = withdrawal_method_title(withdrawal.withdrawal_method)
get_user = await Userx().get_required(user_id=withdrawal.user_id)
await call.message.edit_text(
ded(f"""
<b>✅ Заявка на вывод выполнена</b>
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Пользователь: <b>@{get_user.user_login}</b> | <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
▪️ Сумма: <code>{withdrawal.withdrawal_amount}₽</code>
▪️ Способ: <code>{method_title}</code>
▪️ Реквизиты: <code>{withdrawal.withdrawal_recipient}</code>
"""),
reply_markup=None,
)
try:
await bot.send_message(
withdrawal.user_id,
ded(f"""
<b>✅ Заявка на вывод выполнена</b>
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Сумма: <code>{withdrawal.withdrawal_amount}₽</code>
▪️ Способ: <code>{method_title}</code>
"""),
)
except Exception:
bot_logger.warning(
"Не удалось отправить пользователю уведомление о выполненном выводе",
exc_info=True,
)
await call.answer("✅ Заявка выполнена", True)
@router.callback_query(F.data.startswith("referral_withdrawal_reject:"))
async def referral_withdrawal_reject(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
withdrawal_id_raw = call.data.split(":", 1)[1]
if not withdrawal_id_raw.isdigit():
return await call.answer("❌ Заявка не найдена", True)
status, withdrawal = await ReferralWithdrawalx().reject(
withdrawal_id=int(withdrawal_id_raw),
admin_id=call.from_user.id,
)
if status == "ALREADY_PROCESSED":
return await call.answer("❌ Заявка уже обработана", True)
if status != "ok" or withdrawal is None:
return await call.answer("❌ Не удалось обработать заявку", True)
get_user = await Userx().get_required(user_id=withdrawal.user_id)
method_title = withdrawal_method_title(withdrawal.withdrawal_method)
await call.message.edit_text(
ded(f"""
<b>❌ Заявка на вывод отклонена</b>
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Пользователь: <b>@{get_user.user_login}</b> | <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
▪️ Сумма возвращена на реферальный баланс: <code>{withdrawal.withdrawal_amount}₽</code>
▪️ Способ: <code>{method_title}</code>
▪️ Реквизиты: <code>{withdrawal.withdrawal_recipient}</code>
"""),
reply_markup=None,
)
try:
await bot.send_message(
withdrawal.user_id,
ded(f"""
<b>❌ Заявка на вывод отклонена</b>
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Сумма возвращена на реферальный баланс: <code>{withdrawal.withdrawal_amount}₽</code>
"""),
)
except Exception:
bot_logger.warning(
"Не удалось отправить пользователю уведомление об отклонённом выводе",
exc_info=True,
)
await call.answer("❌ Заявка отклонена", True)
# Поиск чеков и профилей
@router.message(F.text == "🔍 Поиск")
async def functions_find(message: Message, bot: Bot, state: FSM, arSession: ARS):
@@ -55,17 +174,23 @@ async def functions_mail_get(message: Message, bot: Bot, state: FSM, arSession:
# Подтверждение отправки рассылки
@router.callback_query(F.data.startswith("mail_confirm:"), StateFilter("here_mail_confirm"))
async def functions_mail_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
@router.callback_query(
F.data.startswith("mail_confirm:"), StateFilter("here_mail_confirm")
)
async def functions_mail_confirm(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
get_status = call.data.split(":")[1]
send_message = (await state.get_data())['here_mail_message']
send_message = (await state.get_data())["here_mail_message"]
await state.clear()
if get_status == "Yes":
get_users = await Userx().get_all()
await call.message.edit_text(f"<b>📢 Рассылка началась... (0/{len(get_users)})</b>")
await call.message.edit_text(
f"<b>📢 Рассылка началась... (0/{len(get_users)})</b>"
)
await asyncio.create_task(functions_mail_make(bot, send_message, call))
else:
@@ -76,7 +201,7 @@ async def functions_mail_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSe
##################################### ПОИСК ####################################
# Принятие айди/логина пользователя или чека для поиска
@router.message(F.text, StateFilter("here_find"))
@router.message(F.text.lower().startswith(('.find', 'find')))
@router.message(F.text.lower().startswith((".find", "find")))
async def functions_find_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
find_data = message.text.lower()
@@ -116,14 +241,18 @@ async def functions_find_get(message: Message, bot: Bot, state: FSM, arSession:
return await refill_open_admin(bot, message.from_user.id, get_refill)
if get_purchase is not None:
return await purchase_open_admin(bot, arSession, message.from_user.id, get_purchase)
return await purchase_open_admin(
bot, arSession, message.from_user.id, get_purchase
)
################################################################################
############################## УПРАВЛЕНИЕ ПРОФИЛЕМ #############################
# Обновление профиля пользователя
@router.callback_query(F.data.startswith("admin_user_refresh:"))
async def functions_user_refresh(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
async def functions_user_refresh(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
user_id = int(call.data.split(":")[1])
get_user = await Userx().get_required(user_id=user_id)
@@ -136,7 +265,9 @@ async def functions_user_refresh(call: CallbackQuery, bot: Bot, state: FSM, arSe
# Покупки пользователя
@router.callback_query(F.data.startswith("admin_user_purchases:"))
async def functions_user_purchases(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
async def functions_user_purchases(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
user_id = int(call.data.split(":")[1])
get_user = await Userx().get_required(user_id=user_id)
@@ -174,7 +305,9 @@ async def functions_user_purchases(call: CallbackQuery, bot: Bot, state: FSM, ar
# Выдача баланса пользователю
@router.callback_query(F.data.startswith("admin_user_balance_add:"))
async def functions_user_balance_add(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
async def functions_user_balance_add(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user=user_id)
@@ -188,8 +321,10 @@ async def functions_user_balance_add(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие суммы для выдачи баланса пользователю
@router.message(F.text, StateFilter("here_user_add"))
async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
user_id = (await state.get_data())['here_user']
async def functions_user_balance_add_get(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
user_id = (await state.get_data())["here_user"]
if not is_number(message.text):
return await message.answer(
@@ -222,7 +357,11 @@ async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM,
f"<b>💰 Вам было выдано <code>{message.text}₽</code></b>",
)
except Exception:
bot_logger.debug("Не удалось уведомить пользователя %s о выдаче баланса", user_id, exc_info=True)
bot_logger.debug(
"Не удалось уведомить пользователя %s о выдаче баланса",
user_id,
exc_info=True,
)
await message.answer(
f"👤 Пользователь: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
@@ -235,7 +374,9 @@ async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM,
# Изменение баланса пользователю
@router.callback_query(F.data.startswith("admin_user_balance_set:"))
async def functions_user_balance_set(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
async def functions_user_balance_set(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user=user_id)
@@ -249,8 +390,10 @@ async def functions_user_balance_set(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие суммы для изменения баланса пользователя
@router.message(F.text, StateFilter("here_user_set"))
async def functions_user_balance_set_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
user_id = (await state.get_data())['here_user']
async def functions_user_balance_set_get(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
user_id = (await state.get_data())["here_user"]
if not is_number(message.text):
return await message.answer(
@@ -294,7 +437,9 @@ async def functions_user_balance_set_get(message: Message, bot: Bot, state: FSM,
# Отправка сообщения пользователю
@router.callback_query(F.data.startswith("admin_user_message:"))
async def functions_user_user_message(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
async def functions_user_user_message(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user_id=user_id)
@@ -309,17 +454,24 @@ async def functions_user_user_message(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие сообщения для отправки пользователю
@router.message(F.text, StateFilter("here_user_message"))
async def functions_user_user_message_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
user_id = (await state.get_data())['here_user_id']
async def functions_user_user_message_get(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
user_id = (await state.get_data())["here_user_id"]
await state.clear()
get_message = "<b>💌 Сообщение от администратора:</b>\n" + f"<code>{clear_html(message.text)}</code>"
get_message = (
"<b>💌 Сообщение от администратора:</b>\n"
+ f"<code>{clear_html(message.text)}</code>"
)
get_user = await Userx().get_required(user_id=user_id)
try:
await bot.send_message(user_id, get_message)
except Exception:
bot_logger.debug("Не удалось отправить сообщение пользователю %s", user_id, exc_info=True)
bot_logger.debug(
"Не удалось отправить сообщение пользователю %s", user_id, exc_info=True
)
await message.reply("<b>❌ Не удалось отправить сообщение</b>")
else:
await message.reply("<b>✅ Сообщение было успешно доставлено</b>")
+94 -1
View File
@@ -1,11 +1,13 @@
# - *- coding: utf- 8 - *-
from math import isfinite
from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message
from tgbot.database import Settingsx, Userx
from tgbot.keyboards.inline_admin import settings_status_finl, settings_finl
from tgbot.utils.const_functions import ded
from tgbot.utils.const_functions import ded, is_number, to_number
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import FSM, ARS
from tgbot.utils.misc_functions import send_admins, insert_tags
@@ -116,6 +118,31 @@ async def settings_status_refill(
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
# Включение/выключение реферальной системы
@router.callback_query(F.data.startswith("settings_status_referral:"))
async def settings_status_referral(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
get_status = call.data.split(":")[1]
get_user = await Userx().get_required(user_id=call.from_user.id)
await Settingsx().update(status_referral=get_status)
if get_status == "True":
send_text = "🟢 Включил реферальную систему в боте"
else:
send_text = "🔴 Выключил реферальную систему в боте"
await send_admins(
bot,
f"👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
f"{send_text}",
not_me=get_user.user_id,
)
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
# Включение/выключение уведомлений о покупках
@router.callback_query(F.data.startswith("settings_notification_buy:"))
async def settings_notification_buy(
@@ -267,6 +294,72 @@ async def settings_faq_get(message: Message, bot: Bot, state: FSM, arSession: AR
# Принятие поддержки
@router.callback_query(F.data == "settings_edit_referral_bonus")
async def settings_edit_referral_bonus(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
await state.clear()
await state.set_state("here_settings_referral_bonus")
await call.message.edit_text(
"<b>🤝 Введите фиксированный бонус за приглашение в рублях</b>"
)
@router.callback_query(F.data == "settings_edit_referral_percent")
async def settings_edit_referral_percent(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
await state.clear()
await state.set_state("here_settings_referral_percent")
await call.message.edit_text(
"<b>📈 Введите процент дохода с пополнений от 0 до 100</b>"
)
@router.message(F.text, StateFilter("here_settings_referral_bonus"))
async def settings_referral_bonus_get(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
if not is_number(message.text):
return await message.answer(
"<b>❌ Введите неотрицательное число в рублях</b>"
)
referral_bonus = float(to_number(message.text))
if not isfinite(referral_bonus) or referral_bonus < 0:
return await message.answer(
"<b>❌ Введите неотрицательное число в рублях</b>"
)
await Settingsx().update(referral_bonus_rub=round(referral_bonus, 2))
await state.clear()
await message.answer(
"<b>🖍 Изменение данных бота</b>",
reply_markup=await settings_finl(),
)
@router.message(F.text, StateFilter("here_settings_referral_percent"))
async def settings_referral_percent_get(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
if not is_number(message.text):
return await message.answer("<b>❌ Введите число от 0 до 100</b>")
referral_percent = float(to_number(message.text))
if not isfinite(referral_percent) or not 0 <= referral_percent <= 100:
return await message.answer("<b>❌ Введите число от 0 до 100</b>")
await Settingsx().update(referral_refill_percent=round(referral_percent, 2))
await state.clear()
await message.answer(
"<b>🖍 Изменение данных бота</b>",
reply_markup=await settings_finl(),
)
@router.message(F.text, StateFilter("here_settings_support"))
async def settings_support_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
get_support = message.text
+3 -1
View File
@@ -124,7 +124,9 @@ async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS):
async def main_start_deeplink(message: Message, bot: Bot, state: FSM, arSession: ARS):
deepling_args = message.text[7:]
if deepling_args.startswith("p_"):
if deepling_args.startswith("r_"):
await main_start(message, bot, state, arSession)
elif deepling_args.startswith("p_"):
position_id_raw = deepling_args[2:]
if not position_id_raw.isdigit():
+359
View File
@@ -0,0 +1,359 @@
# - *- coding: utf- 8 - *-
from math import isfinite
from urllib.parse import urlparse
import re
from aiogram import Bot, F, Router
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message
from tgbot.database import ReferralWithdrawalx, Referralx, Settingsx, Userx
from tgbot.keyboards.inline_admin import referral_withdrawal_actions_finl
from tgbot.keyboards.inline_user import (
referral_menu_finl,
referral_transfer_method_finl,
referral_withdrawal_method_finl,
)
from tgbot.utils.const_functions import clear_html, ded, is_number, to_number
from tgbot.utils.misc.bot_models import ARS, FSM
from tgbot.utils.misc_functions import send_admins
router = Router(name=__name__)
def is_valid_withdrawal_recipient(withdrawal_method: str, recipient: str) -> bool:
if withdrawal_method == "Cryptobot":
return bool(re.fullmatch(r"@[A-Za-z][A-Za-z0-9_]{4,31}", recipient))
if withdrawal_method == "Lolzteam":
parsed_url = urlparse(recipient)
return parsed_url.scheme in ("http", "https") and parsed_url.hostname in (
"zelenka.guru",
"lolz.team",
"lolz.live",
"lolz.guru",
)
return False
def withdrawal_recipient_hint(withdrawal_method: str) -> str:
if withdrawal_method == "Cryptobot":
return (
"<b>📥 Введите @username получателя в CryptoBot</b>\n"
"➖➖➖➖➖➖➖➖➖➖\n"
"<i>Пример: @username</i>"
)
return (
"<b>📥 Введите ссылку на профиль получателя Lolzteam</b>\n"
"➖➖➖➖➖➖➖➖➖➖\n"
"<i>Подойдут ссылки с zelenka.guru, lolz.team, lolz.live или lolz.guru</i>"
)
def withdrawal_method_title(withdrawal_method: str) -> str:
if withdrawal_method == "Cryptobot":
return "CryptoBot"
return withdrawal_method
async def get_referral_menu_data(user_id: int, bot: Bot):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
return None
get_user = await Userx().get_required(user_id=user_id)
get_referrals = await Referralx().gets(referrer_id=user_id)
bot_username = get_settings.misc_bot
if bot_username == "None":
bot_username = (await bot.get_me()).username or ""
referral_link = f"https://t.me/{bot_username}?start=r_{user_id}"
available_balance = round(
get_user.user_referral_balance - get_user.user_referral_hold,
2,
)
referral_text = ded(f"""
<b>🤝 Реферальная система</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Приглашено пользователей: <code>{len(get_referrals)}</code>
▪️ Реферальный баланс: <code>{get_user.user_referral_balance}₽</code>
▪️ Доступно: <code>{available_balance}₽</code>
➖➖➖➖➖➖➖➖➖➖
▪️ Бонус за приглашение: <code>{get_settings.referral_bonus_rub}₽</code>
▪️ Доход с пополнений: <code>{get_settings.referral_refill_percent}%</code>
""")
return referral_text, referral_menu_finl(referral_link)
@router.message(F.text == "🤝 Реферальная система")
async def show_referral_menu(
message: Message,
bot: Bot,
state: FSM,
arSession: ARS,
user_id: int | None = None,
):
await state.clear()
user_id = user_id or message.from_user.id
referral_menu_data = await get_referral_menu_data(user_id, bot)
if referral_menu_data is None:
return await message.answer("<b>⛔ Реферальная система временно отключена</b>")
referral_text, referral_keyboard = referral_menu_data
await message.answer(
referral_text,
reply_markup=referral_keyboard,
disable_web_page_preview=True,
)
@router.callback_query(F.data == "referral_menu")
async def user_referral_menu_return(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
await state.clear()
referral_menu_data = await get_referral_menu_data(call.from_user.id, bot)
if referral_menu_data is None:
return await call.answer("⛔ Реферальная система временно отключена", True)
referral_text, referral_keyboard = referral_menu_data
await call.message.edit_text(
referral_text,
reply_markup=referral_keyboard,
disable_web_page_preview=True,
)
@router.callback_query(F.data == "referral_transfer")
async def user_referral_transfer_start(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await call.answer("⛔ Реферальная система временно отключена", True)
await state.clear()
await state.set_state("here_referral_transfer_amount")
await call.message.edit_text(
"<b>💸 Введите сумму для перевода на основной баланс</b>",
reply_markup=referral_transfer_method_finl(),
)
@router.callback_query(F.data == "referral_withdrawal")
async def user_referral_withdrawal_start(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await call.answer("⛔ Реферальная система временно отключена", True)
await state.clear()
await call.message.edit_text(
"<b>📥 Выберите способ вывода средств</b>",
reply_markup=referral_withdrawal_method_finl(),
)
@router.callback_query(F.data.startswith("referral_withdrawal_method:"))
async def user_referral_withdrawal_method(
call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
):
withdrawal_method = call.data.split(":", 1)[1]
if withdrawal_method not in ("Cryptobot", "Lolzteam"):
return await call.answer("❌ Способ вывода не найден", True)
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await call.answer("⛔ Реферальная система временно отключена", True)
await state.clear()
await state.update_data(here_referral_withdrawal_method=withdrawal_method)
await state.set_state("here_referral_withdrawal_amount")
await call.message.edit_text("<b>📥 Введите сумму для вывода</b>")
@router.message(F.text, StateFilter("here_referral_withdrawal_amount"))
async def user_referral_withdrawal_amount(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await message.answer("<b>⛔ Реферальная система временно отключена</b>")
if not is_number(message.text):
return await message.answer("<b>❌ Введите положительную сумму в рублях</b>")
withdrawal_amount = float(to_number(message.text))
if not isfinite(withdrawal_amount) or withdrawal_amount <= 0:
return await message.answer("<b>❌ Введите положительную сумму в рублях</b>")
get_user = await Userx().get_required(user_id=message.from_user.id)
available_balance = round(
get_user.user_referral_balance - get_user.user_referral_hold, 2
)
if withdrawal_amount > available_balance:
return await message.answer(
"<b>❌ Недостаточно доступных средств на реферальном балансе</b>"
)
withdrawal_data = await state.get_data()
withdrawal_method = withdrawal_data.get("here_referral_withdrawal_method")
if withdrawal_method not in ("Cryptobot", "Lolzteam"):
await state.clear()
return await message.answer("<b>❌ Данные вывода устарели. Начните заново</b>")
await state.update_data(here_referral_withdrawal_amount=withdrawal_amount)
await state.set_state("here_referral_withdrawal_recipient")
await message.answer(withdrawal_recipient_hint(withdrawal_method))
@router.message(F.text, StateFilter("here_referral_withdrawal_recipient"))
async def user_referral_withdrawal_recipient(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await message.answer("<b>⛔ Реферальная система временно отключена</b>")
withdrawal_data = await state.get_data()
withdrawal_method = withdrawal_data.get("here_referral_withdrawal_method")
withdrawal_amount = withdrawal_data.get("here_referral_withdrawal_amount")
if withdrawal_method is None or withdrawal_amount is None:
await state.clear()
return await message.answer("<b>❌ Данные вывода устарели. Начните заново</b>")
withdrawal_recipient = clear_html(message.text).strip()
if not withdrawal_recipient:
return await message.answer(
"<b>❌ Введите реквизиты</b>\n"
f"{withdrawal_recipient_hint(withdrawal_method)}"
)
if not is_valid_withdrawal_recipient(withdrawal_method, withdrawal_recipient):
return await message.answer(
"<b>❌ Некорректные реквизиты</b>\n"
f"{withdrawal_recipient_hint(withdrawal_method)}"
)
withdrawal_status, withdrawal = await ReferralWithdrawalx().create_pending(
user_id=message.from_user.id,
withdrawal_amount=withdrawal_amount,
withdrawal_method=withdrawal_method,
withdrawal_recipient=withdrawal_recipient,
)
if withdrawal_status == "INSUFFICIENT_FUNDS":
return await message.answer(
"<b>❌ Недостаточно доступных средств на реферальном балансе</b>"
)
if withdrawal_status in ("INVALID_AMOUNT", "INVALID_RECIPIENT", "INVALID_METHOD"):
return await message.answer("<b>❌ Некорректные данные для вывода</b>")
if withdrawal_status == "USER_NOT_FOUND":
await state.clear()
return await message.answer(
"<b>❌ Пользователь не найден. Обратитесь в поддержку</b>"
)
if withdrawal_status != "ok" or withdrawal is None:
return await message.answer(
"<b>❌ Не удалось создать заявку. Попробуйте позже</b>"
)
await state.clear()
get_user = await Userx().get_required(user_id=message.from_user.id)
method_title = withdrawal_method_title(withdrawal.withdrawal_method)
await message.answer(
ded(f"""
<b>📥 Заявка на вывод создана</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Сумма: <code>{withdrawal.withdrawal_amount}₽</code>
▪️ Способ: <code>{method_title}</code>
"""),
)
await send_admins(
bot,
ded(f"""
<b>📥 Новая заявка на вывод</b>
▪️ Номер заявки: <code>#{withdrawal.increment}</code>
▪️ Пользователь: <b>@{get_user.user_login}</b> | <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
▪️ Сумма: <code>{withdrawal.withdrawal_amount}₽</code>
▪️ Способ: <code>{method_title}</code>
▪️ Реквизиты: <code>{withdrawal.withdrawal_recipient}</code>
"""),
keyboard=referral_withdrawal_actions_finl(withdrawal.increment),
)
@router.message(F.text, StateFilter("here_referral_transfer_amount"))
async def user_referral_transfer_amount(
message: Message, bot: Bot, state: FSM, arSession: ARS
):
get_settings = await Settingsx().get()
if get_settings.status_referral != "True":
await state.clear()
return await message.answer("<b>⛔ Реферальная система временно отключена</b>")
if not is_number(message.text):
return await message.answer("<b>❌ Введите положительную сумму в рублях</b>")
transfer_amount = float(to_number(message.text))
if not isfinite(transfer_amount) or transfer_amount <= 0:
return await message.answer("<b>❌ Введите положительную сумму в рублях</b>")
transfer_status = await Referralx().transfer_to_main_balance(
user_id=message.from_user.id,
amount=transfer_amount,
)
if transfer_status == "INSUFFICIENT_FUNDS":
return await message.answer(
"<b>❌ Недостаточно доступных средств на реферальном балансе</b>"
)
if transfer_status == "INVALID_AMOUNT":
return await message.answer("<b>❌ Введите положительную сумму в рублях</b>")
if transfer_status == "USER_NOT_FOUND":
await state.clear()
return await message.answer(
"<b>❌ Пользователь не найден. Обратитесь в поддержку</b>"
)
if transfer_status != "ok":
return await message.answer(
"<b>❌ Не удалось выполнить перевод. Попробуйте позже</b>"
)
await state.clear()
get_user = await Userx().get_required(user_id=message.from_user.id)
await message.answer(
ded(f"""
<b>💸 Средства переведены на основной баланс</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Переведено: <code>{round(transfer_amount, 2)}₽</code>
▪️ Основной баланс: <code>{get_user.user_balance}₽</code>
▪️ Реферальный баланс: <code>{get_user.user_referral_balance}₽</code>
"""),
)
+23 -1
View File
@@ -5,7 +5,7 @@ from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message, PreCheckoutQuery
from tgbot.database import Paymentsx, Refillx, Userx, Settingsx
from tgbot.database import Paymentsx, ReferralTransactionx, Refillx, Userx, Settingsx
from tgbot.keyboards.inline_user import refill_bill_finl, refill_method_finl
from tgbot.services.api_cryptobot import CryptobotAPI
from tgbot.services.api_lolzteam import LolzteamAPI
@@ -459,4 +459,26 @@ async def save_refill_success(
"""),
)
referral_reward = await ReferralTransactionx().get_refill_reward(
f"refill:{pay_comment or pay_receipt}"
)
if referral_reward is not None:
try:
await bot.send_message(
referral_reward.user_id,
ded(f"""
<b>🤝 Реферальное начисление</b>
▪️ Ваш реферал: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>
▪️ Пополнение: <code>{pay_amount}₽</code>
▪️ Начислено: <code>{referral_reward.amount}₽</code>
"""),
)
except Exception:
bot_logger.warning(
"Не удалось отправить уведомление о реферальном начислении",
exc_info=True,
)
return response_success, pay_receipt