mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-12 15:10:29 +00:00
421 lines
13 KiB
Python
421 lines
13 KiB
Python
from datetime import datetime, timedelta
|
|
from typing import Optional, List
|
|
from enum import Enum
|
|
|
|
from sqlalchemy import (
|
|
Column, Integer, String, DateTime, Boolean, Text,
|
|
ForeignKey, Float, JSON, BigInteger
|
|
)
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import relationship, Mapped
|
|
from sqlalchemy.sql import func
|
|
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
class UserStatus(Enum):
|
|
ACTIVE = "active"
|
|
BLOCKED = "blocked"
|
|
DELETED = "deleted"
|
|
|
|
|
|
class SubscriptionStatus(Enum):
|
|
TRIAL = "trial"
|
|
ACTIVE = "active"
|
|
EXPIRED = "expired"
|
|
DISABLED = "disabled"
|
|
|
|
|
|
class TransactionType(Enum):
|
|
DEPOSIT = "deposit"
|
|
WITHDRAWAL = "withdrawal"
|
|
SUBSCRIPTION_PAYMENT = "subscription_payment"
|
|
REFUND = "refund" # Возврат
|
|
REFERRAL_REWARD = "referral_reward"
|
|
|
|
|
|
class PromoCodeType(Enum):
|
|
BALANCE = "balance"
|
|
SUBSCRIPTION_DAYS = "subscription_days"
|
|
TRIAL_SUBSCRIPTION = "trial_subscription"
|
|
|
|
|
|
class PaymentMethod(Enum):
|
|
TELEGRAM_STARS = "telegram_stars"
|
|
TRIBUTE = "tribute"
|
|
MANUAL = "manual"
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
telegram_id = Column(BigInteger, unique=True, index=True, nullable=False)
|
|
username = Column(String(255), nullable=True)
|
|
first_name = Column(String(255), nullable=True)
|
|
last_name = Column(String(255), nullable=True)
|
|
|
|
status = Column(String(20), default=UserStatus.ACTIVE.value)
|
|
language = Column(String(5), default="ru")
|
|
|
|
balance_kopeks = Column(Integer, default=0)
|
|
|
|
used_promocodes = Column(Integer, default=0)
|
|
|
|
has_had_paid_subscription = Column(Boolean, default=False, nullable=False)
|
|
|
|
referred_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
referral_code = Column(String(20), unique=True, nullable=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
last_activity = Column(DateTime, default=func.now())
|
|
|
|
remnawave_uuid = Column(String(255), nullable=True, unique=True)
|
|
|
|
broadcasts = relationship("BroadcastHistory", back_populates="admin")
|
|
|
|
referrals = relationship("User", backref="referrer", remote_side=[id], foreign_keys="User.referred_by_id")
|
|
subscription = relationship("Subscription", back_populates="user", uselist=False)
|
|
transactions = relationship("Transaction", back_populates="user")
|
|
referral_earnings = relationship("ReferralEarning", foreign_keys="ReferralEarning.user_id", back_populates="user")
|
|
|
|
@property
|
|
def balance_rubles(self) -> float:
|
|
return self.balance_kopeks / 100
|
|
|
|
@property
|
|
def full_name(self) -> str:
|
|
parts = [self.first_name, self.last_name]
|
|
return " ".join(filter(None, parts)) or self.username or f"ID{self.telegram_id}"
|
|
|
|
def add_balance(self, kopeks: int) -> None:
|
|
self.balance_kopeks += kopeks
|
|
|
|
def subtract_balance(self, kopeks: int) -> bool:
|
|
if self.balance_kopeks >= kopeks:
|
|
self.balance_kopeks -= kopeks
|
|
return True
|
|
return False
|
|
|
|
|
|
class Subscription(Base):
|
|
__tablename__ = "subscriptions"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
|
|
status = Column(String(20), default=SubscriptionStatus.TRIAL.value)
|
|
is_trial = Column(Boolean, default=True)
|
|
|
|
start_date = Column(DateTime, default=func.now())
|
|
end_date = Column(DateTime, nullable=False)
|
|
|
|
traffic_limit_gb = Column(Integer, default=0)
|
|
traffic_used_gb = Column(Float, default=0.0)
|
|
|
|
subscription_url = Column(String, nullable=True)
|
|
|
|
device_limit = Column(Integer, default=1)
|
|
|
|
connected_squads = Column(JSON, default=list)
|
|
|
|
autopay_enabled = Column(Boolean, default=False)
|
|
autopay_days_before = Column(Integer, default=3)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
remnawave_short_uuid = Column(String(255), nullable=True)
|
|
|
|
user = relationship("User", back_populates="subscription")
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
return (
|
|
self.status == SubscriptionStatus.ACTIVE.value and
|
|
self.end_date > datetime.utcnow()
|
|
)
|
|
|
|
@property
|
|
def is_expired(self) -> bool:
|
|
return self.end_date <= datetime.utcnow()
|
|
|
|
@property
|
|
def days_left(self) -> int:
|
|
if self.is_expired:
|
|
return 0
|
|
delta = self.end_date - datetime.utcnow()
|
|
return delta.days
|
|
|
|
@property
|
|
def traffic_used_percent(self) -> float:
|
|
if self.traffic_limit_gb == 0:
|
|
return 0.0
|
|
if self.traffic_limit_gb > 0:
|
|
return min((self.traffic_used_gb / self.traffic_limit_gb) * 100, 100.0)
|
|
return 0.0
|
|
|
|
def extend_subscription(self, days: int):
|
|
from datetime import timedelta, datetime
|
|
|
|
if self.end_date > datetime.utcnow():
|
|
self.end_date = self.end_date + timedelta(days=days)
|
|
else:
|
|
self.end_date = datetime.utcnow() + timedelta(days=days)
|
|
|
|
if self.status == SubscriptionStatus.EXPIRED.value:
|
|
self.status = SubscriptionStatus.ACTIVE.value
|
|
|
|
def add_traffic(self, gb: int):
|
|
if self.traffic_limit_gb == 0:
|
|
return
|
|
self.traffic_limit_gb += gb
|
|
|
|
|
|
class Transaction(Base):
|
|
__tablename__ = "transactions"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
|
|
type = Column(String(50), nullable=False)
|
|
amount_kopeks = Column(Integer, nullable=False)
|
|
description = Column(Text, nullable=True)
|
|
|
|
payment_method = Column(String(50), nullable=True)
|
|
external_id = Column(String(255), nullable=True)
|
|
|
|
is_completed = Column(Boolean, default=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
completed_at = Column(DateTime, nullable=True)
|
|
|
|
user = relationship("User", back_populates="transactions")
|
|
|
|
@property
|
|
def amount_rubles(self) -> float:
|
|
return self.amount_kopeks / 100
|
|
|
|
|
|
class PromoCode(Base):
|
|
__tablename__ = "promocodes"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
code = Column(String(50), unique=True, nullable=False, index=True)
|
|
type = Column(String(50), nullable=False)
|
|
|
|
balance_bonus_kopeks = Column(Integer, default=0)
|
|
subscription_days = Column(Integer, default=0)
|
|
|
|
max_uses = Column(Integer, default=1)
|
|
current_uses = Column(Integer, default=0)
|
|
|
|
valid_from = Column(DateTime, default=func.now())
|
|
valid_until = Column(DateTime, nullable=True)
|
|
|
|
is_active = Column(Boolean, default=True)
|
|
|
|
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
uses = relationship("PromoCodeUse", back_populates="promocode")
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
now = datetime.utcnow()
|
|
return (
|
|
self.is_active and
|
|
self.current_uses < self.max_uses and
|
|
self.valid_from <= now and
|
|
(self.valid_until is None or self.valid_until >= now)
|
|
)
|
|
|
|
@property
|
|
def uses_left(self) -> int:
|
|
return max(0, self.max_uses - self.current_uses)
|
|
|
|
|
|
class PromoCodeUse(Base):
|
|
__tablename__ = "promocode_uses"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
promocode_id = Column(Integer, ForeignKey("promocodes.id"), nullable=False)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
|
|
used_at = Column(DateTime, default=func.now())
|
|
|
|
promocode = relationship("PromoCode", back_populates="uses")
|
|
user = relationship("User")
|
|
|
|
|
|
class ReferralEarning(Base):
|
|
__tablename__ = "referral_earnings"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
referral_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
|
|
amount_kopeks = Column(Integer, nullable=False)
|
|
reason = Column(String(100), nullable=False)
|
|
|
|
referral_transaction_id = Column(Integer, ForeignKey("transactions.id"), nullable=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
|
|
user = relationship("User", foreign_keys=[user_id], back_populates="referral_earnings")
|
|
referral = relationship("User", foreign_keys=[referral_id])
|
|
referral_transaction = relationship("Transaction")
|
|
|
|
@property
|
|
def amount_rubles(self) -> float:
|
|
return self.amount_kopeks / 100
|
|
|
|
|
|
class Squad(Base):
|
|
__tablename__ = "squads"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
uuid = Column(String(255), unique=True, nullable=False)
|
|
name = Column(String(255), nullable=False)
|
|
country_code = Column(String(5), nullable=True)
|
|
|
|
is_available = Column(Boolean, default=True)
|
|
price_kopeks = Column(Integer, default=0)
|
|
|
|
description = Column(Text, nullable=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
@property
|
|
def price_rubles(self) -> float:
|
|
return self.price_kopeks / 100
|
|
|
|
|
|
class ServiceRule(Base):
|
|
__tablename__ = "service_rules"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
order = Column(Integer, default=0)
|
|
title = Column(String(255), nullable=False)
|
|
|
|
content = Column(Text, nullable=False)
|
|
|
|
is_active = Column(Boolean, default=True)
|
|
|
|
language = Column(String(5), default="ru")
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
|
|
class SystemSetting(Base):
|
|
__tablename__ = "system_settings"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
key = Column(String(255), unique=True, nullable=False)
|
|
value = Column(Text, nullable=True)
|
|
description = Column(Text, nullable=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
|
|
class MonitoringLog(Base):
|
|
__tablename__ = "monitoring_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
event_type = Column(String(100), nullable=False)
|
|
|
|
message = Column(Text, nullable=False)
|
|
data = Column(JSON, nullable=True)
|
|
|
|
is_success = Column(Boolean, default=True)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
|
|
class BroadcastHistory(Base):
|
|
__tablename__ = "broadcast_history"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
target_type = Column(String(100), nullable=False)
|
|
message_text = Column(Text, nullable=False)
|
|
total_count = Column(Integer, default=0)
|
|
sent_count = Column(Integer, default=0)
|
|
failed_count = Column(Integer, default=0)
|
|
status = Column(String(50), default="in_progress")
|
|
admin_id = Column(Integer, ForeignKey("users.id"))
|
|
admin_name = Column(String(255))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
admin = relationship("User", back_populates="broadcasts")
|
|
|
|
class ServerSquad(Base):
|
|
__tablename__ = "server_squads"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
squad_uuid = Column(String(255), unique=True, nullable=False, index=True)
|
|
|
|
display_name = Column(String(255), nullable=False)
|
|
|
|
original_name = Column(String(255), nullable=True)
|
|
|
|
country_code = Column(String(5), nullable=True)
|
|
|
|
is_available = Column(Boolean, default=True)
|
|
|
|
price_kopeks = Column(Integer, default=0)
|
|
|
|
description = Column(Text, nullable=True)
|
|
|
|
sort_order = Column(Integer, default=0)
|
|
|
|
max_users = Column(Integer, nullable=True)
|
|
current_users = Column(Integer, default=0)
|
|
|
|
created_at = Column(DateTime, default=func.now())
|
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
|
|
|
@property
|
|
def price_rubles(self) -> float:
|
|
return self.price_kopeks / 100
|
|
|
|
@property
|
|
def is_full(self) -> bool:
|
|
if self.max_users is None:
|
|
return False
|
|
return self.current_users >= self.max_users
|
|
|
|
@property
|
|
def availability_status(self) -> str:
|
|
if not self.is_available:
|
|
return "Недоступен"
|
|
elif self.is_full:
|
|
return "Переполнен"
|
|
else:
|
|
return "Доступен"
|
|
|
|
|
|
class SubscriptionServer(Base):
|
|
__tablename__ = "subscription_servers"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
subscription_id = Column(Integer, ForeignKey("subscriptions.id"), nullable=False)
|
|
server_squad_id = Column(Integer, ForeignKey("server_squads.id"), nullable=False)
|
|
|
|
connected_at = Column(DateTime, default=func.now())
|
|
|
|
paid_price_kopeks = Column(Integer, default=0)
|
|
|
|
subscription = relationship("Subscription", backref="subscription_servers")
|
|
server_squad = relationship("ServerSquad", backref="subscription_servers") |